Skip to content

Commit

Permalink
Support Async Lifecycles (#214)
Browse files Browse the repository at this point in the history
* Add 5.9 manifest with strict concurrency

* Add async lifecycle handler function

* Fix a ton of Sendable warnings

* Rework RedisStorage

* Silence some warnings

* Fix Sendable warnings

* Update CI

* Fine

* Modernize/fix CI

* Disable CodeQL for now

---------

Co-authored-by: Gwynne Raskind <gwynne@vapor.codes>
  • Loading branch information
0xTim and gwynne authored May 24, 2024
1 parent 44e5774 commit 383ed57
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 94 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/api-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ jobs:
with:
package_name: redis
modules: Redis
pathsToInvalidate: /redis
pathsToInvalidate: /redis/*
58 changes: 35 additions & 23 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,47 @@ jobs:
api-breakage:
if: ${{ !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
container: swift:5.8-jammy
container: swift:jammy
steps:
- name: Check out code
uses: actions/checkout@v3
uses: actions/checkout@v4
with: { 'fetch-depth': 0 }
- name: Run API breakage check action
uses: vapor/ci/.github/actions/ci-swift-check-api-breakage@reusable-workflows
- name: Run API breakage check
run: |
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
swift package diagnose-api-breaking-changes origin/main
# gh-codeql:
# if: ${{ !(github.event.pull_request.draft || false) }}
# runs-on: ubuntu-latest
# permissions: { actions: write, contents: read, security-events: write }
# timeout-minutes: 30
# steps:
# - name: Install latest Swift toolchain
# uses: vapor/swiftly-action@v0.1
# with: { toolchain: latest }
# - name: Check out code
# uses: actions/checkout@v4
# - name: Fix Git configuration
# run: 'git config --global --add safe.directory "${GITHUB_WORKSPACE}"'
# - name: Initialize CodeQL
# uses: github/codeql-action/init@v3
# with: { languages: swift }
# - name: Perform build
# run: swift build
# - name: Run CodeQL analyze
# uses: github/codeql-action/analyze@v3

linux-unit:
if: ${{ !(github.event.pull_request.draft || false) }}
strategy:
fail-fast: false
matrix:
container:
- swift:5.6-focal
- swift:5.7-jammy
- swift:5.8-jammy
- swiftlang/swift:nightly-5.9-jammy
- swift:5.8-focal
- swift:5.9-jammy
- swift:5.10-jammy
- swiftlang/swift:nightly-6.0-jammy
- swiftlang/swift:nightly-main-jammy
redis:
- redis:6
Expand All @@ -47,22 +70,11 @@ jobs:
redis-2:
image: ${{ matrix.redis }}
steps:
- name: Save Redis version to env
run: |
echo REDIS_VERSION='${{ matrix.redis }}' >> $GITHUB_ENV
- name: Display versions
shell: bash
run: |
if [[ '${{ contains(matrix.container, 'nightly') }}' == 'true' ]]; then
SWIFT_PLATFORM="$(source /etc/os-release && echo "${ID}${VERSION_ID}")" SWIFT_VERSION="$(cat /.swift_tag)"
printf 'SWIFT_PLATFORM=%s\nSWIFT_VERSION=%s\n' "${SWIFT_PLATFORM}" "${SWIFT_VERSION}" >>"${GITHUB_ENV}"
fi
printf 'OS: %s\nTag: %s\nVersion:\n' "${SWIFT_PLATFORM}-${RUNNER_ARCH}" "${SWIFT_VERSION}" && swift --version
- name: Check out package
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run unit tests with Thread Sanitizer and coverage
run: swift test --sanitize=thread --enable-code-coverage
- name: Submit coverage report to Codecov.io
uses: vapor/swift-codecov-action@v0.2
- name: Upload coverage data
uses: vapor/swift-codecov-action@v0.3
with:
cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,REDIS_VERSION'
codecov_token: ${{ secrets.CODECOV_TOKEN }}
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.6
// swift-tools-version:5.8
import PackageDescription

let package = Package(
Expand All @@ -14,7 +14,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/swift-server/RediStack.git", from: "1.4.1"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.77.1"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.100.0"),
],
targets: [
.target(
Expand Down
37 changes: 37 additions & 0 deletions Package@swift-5.9.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// swift-tools-version:5.9
import PackageDescription

let package = Package(
name: "redis",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
],
products: [
.library(name: "Redis", targets: ["Redis"])
],
dependencies: [
.package(url: "https://github.com/swift-server/RediStack.git", from: "1.4.1"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.100.0"),
],
targets: [
.target(
name: "Redis",
dependencies: [
.product(name: "RediStack", package: "RediStack"),
.product(name: "Vapor", package: "vapor"),
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
),
.testTarget(
name: "RedisTests",
dependencies: [
.target(name: "Redis"),
.product(name: "XCTVapor", package: "vapor"),
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
)
]
)
4 changes: 2 additions & 2 deletions Sources/Redis/Application.Redis+PubSub.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Vapor
import RediStack
@preconcurrency import RediStack

extension Application.Redis {
private struct PubSubKey: StorageKey, LockKey {
typealias Value = [RedisID: RedisClient]
typealias Value = [RedisID: RedisClient & Sendable]
}

var pubsubClient: RedisClient {
Expand Down
24 changes: 17 additions & 7 deletions Sources/Redis/Redis+Cache.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Vapor
import Foundation
import RediStack
@preconcurrency import RediStack
import NIOCore

// MARK: RedisCacheCoder
Expand Down Expand Up @@ -40,6 +40,11 @@ extension Application.Caches {

/// A cache configured for a given Redis ID and using the provided encoder and decoder.
public func redis<E: RedisCacheEncoder, D: RedisCacheDecoder>(_ id: RedisID = .default, encoder: E, decoder: D) -> Cache {
RedisCache(encoder: FakeSendable(value: encoder), decoder: FakeSendable(value: decoder), client: self.application.redis(id))
}

/// A cache configured for a given Redis ID and using the provided encoder and decoder wrapped as FakeSendable.
func redis(_ id: RedisID = .default, encoder: FakeSendable<some RedisCacheEncoder>, decoder: FakeSendable<some RedisCacheDecoder>) -> Cache {
RedisCache(encoder: encoder, decoder: decoder, client: self.application.redis(id))
}
}
Expand All @@ -59,20 +64,25 @@ extension Application.Caches.Provider {

/// Configures the application cache to use the given Redis ID and the provided encoder and decoder.
public static func redis<E: RedisCacheEncoder, D: RedisCacheDecoder>(_ id: RedisID = .default, encoder: E, decoder: D) -> Self {
.init { $0.caches.use { $0.caches.redis(id, encoder: encoder, decoder: decoder) } }
let wrappedEncoder = FakeSendable(value: encoder)
let wrappedDecoder = FakeSendable(value: decoder)
return .init { $0.caches.use { $0.caches.redis(id, encoder: wrappedEncoder, decoder: wrappedDecoder) } }
}
}

// MARK: - Redis cache driver

/// A wrapper to silence `Sendable` warnings for `JSONDecoder` and `JSONEncoder` when not on macOS.
struct FakeSendable<T>: @unchecked Sendable { let value: T }

/// `Cache` driver for storing cache data in Redis, using a provided encoder and decoder to serialize and deserialize values respectively.
private struct RedisCache<CacheEncoder: RedisCacheEncoder, CacheDecoder: RedisCacheDecoder>: Cache {
let encoder: CacheEncoder
let decoder: CacheDecoder
private struct RedisCache<CacheEncoder: RedisCacheEncoder, CacheDecoder: RedisCacheDecoder>: Cache, Sendable {
let encoder: FakeSendable<CacheEncoder>
let decoder: FakeSendable<CacheDecoder>
let client: RedisClient

func get<T: Decodable>(_ key: String, as type: T.Type) -> EventLoopFuture<T?> {
self.client.get(RedisKey(key), as: CacheDecoder.Input.self).optionalFlatMapThrowing { try self.decoder.decode(T.self, from: $0) }
self.client.get(RedisKey(key), as: CacheDecoder.Input.self).optionalFlatMapThrowing { try self.decoder.value.decode(T.self, from: $0) }
}

func set<T: Encodable>(_ key: String, to value: T?, expiresIn expirationTime: CacheExpirationTime?) -> EventLoopFuture<Void> {
Expand All @@ -81,7 +91,7 @@ private struct RedisCache<CacheEncoder: RedisCacheEncoder, CacheDecoder: RedisCa
}

return self.client.eventLoop
.tryFuture { try self.encoder.encode(value) }
.tryFuture { try self.encoder.value.encode(value) }
.flatMap {
if let expirationTime = expirationTime {
return self.client.setex(RedisKey(key), to: $0, expirationInSeconds: expirationTime.seconds)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Redis/Redis+Sessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import RediStack
import NIOCore

/// A delegate object that controls key behavior of an `Application.Redis.Sessions` driver.
public protocol RedisSessionsDelegate {
public protocol RedisSessionsDelegate: Sendable {
/// Makes a new session ID token.
/// - Note: This method is optional to implement.
///
Expand Down
11 changes: 6 additions & 5 deletions Sources/Redis/RedisConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import NIOSSL
import NIOPosix
import Logging
import NIOCore
import RediStack
@preconcurrency import RediStack

/// Configuration for connecting to a Redis instance
public struct RedisConfiguration {
public struct RedisConfiguration: Sendable {
public typealias ValidationError = RedisConnection.Configuration.ValidationError

public var serverAddresses: [SocketAddress]
Expand All @@ -16,21 +16,22 @@ public struct RedisConfiguration {
public var tlsConfiguration: TLSConfiguration?
public var tlsHostname: String?

public struct PoolOptions {
public struct PoolOptions: Sendable {
public var maximumConnectionCount: RedisConnectionPoolSize
public var minimumConnectionCount: Int
public var connectionBackoffFactor: Float32
public var initialConnectionBackoffDelay: TimeAmount
public var connectionRetryTimeout: TimeAmount?
public var onUnexpectedConnectionClose: ((RedisConnection) -> Void)?
public var onUnexpectedConnectionClose: (@Sendable (RedisConnection) -> Void)?

@preconcurrency
public init(
maximumConnectionCount: RedisConnectionPoolSize = .maximumActiveConnections(2),
minimumConnectionCount: Int = 0,
connectionBackoffFactor: Float32 = 2,
initialConnectionBackoffDelay: TimeAmount = .milliseconds(100),
connectionRetryTimeout: TimeAmount? = nil,
onUnexpectedConnectionClose: ((RedisConnection) -> Void)? = nil
onUnexpectedConnectionClose: (@Sendable (RedisConnection) -> Void)? = nil
) {
self.maximumConnectionCount = maximumConnectionCount
self.minimumConnectionCount = minimumConnectionCount
Expand Down
3 changes: 2 additions & 1 deletion Sources/Redis/RedisID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public struct RedisID: Hashable,
ExpressibleByStringLiteral,
ExpressibleByStringInterpolation,
CustomStringConvertible,
Comparable {
Comparable,
Sendable {

public let rawValue: String

Expand Down
Loading

0 comments on commit 383ed57

Please sign in to comment.