Skip to content

Commit 383ed57

Browse files
0xTimgwynne
andauthored
Support Async Lifecycles (#214)
* 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 <[email protected]>
1 parent 44e5774 commit 383ed57

11 files changed

+163
-94
lines changed

.github/workflows/api-docs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ jobs:
1111
with:
1212
package_name: redis
1313
modules: Redis
14-
pathsToInvalidate: /redis
14+
pathsToInvalidate: /redis/*

.github/workflows/test.yml

+35-23
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,47 @@ jobs:
1717
api-breakage:
1818
if: ${{ !(github.event.pull_request.draft || false) }}
1919
runs-on: ubuntu-latest
20-
container: swift:5.8-jammy
20+
container: swift:jammy
2121
steps:
2222
- name: Check out code
23-
uses: actions/checkout@v3
23+
uses: actions/checkout@v4
2424
with: { 'fetch-depth': 0 }
25-
- name: Run API breakage check action
26-
uses: vapor/ci/.github/actions/ci-swift-check-api-breakage@reusable-workflows
25+
- name: Run API breakage check
26+
run: |
27+
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
28+
swift package diagnose-api-breaking-changes origin/main
29+
30+
# gh-codeql:
31+
# if: ${{ !(github.event.pull_request.draft || false) }}
32+
# runs-on: ubuntu-latest
33+
# permissions: { actions: write, contents: read, security-events: write }
34+
# timeout-minutes: 30
35+
# steps:
36+
# - name: Install latest Swift toolchain
37+
# uses: vapor/[email protected]
38+
# with: { toolchain: latest }
39+
# - name: Check out code
40+
# uses: actions/checkout@v4
41+
# - name: Fix Git configuration
42+
# run: 'git config --global --add safe.directory "${GITHUB_WORKSPACE}"'
43+
# - name: Initialize CodeQL
44+
# uses: github/codeql-action/init@v3
45+
# with: { languages: swift }
46+
# - name: Perform build
47+
# run: swift build
48+
# - name: Run CodeQL analyze
49+
# uses: github/codeql-action/analyze@v3
2750

2851
linux-unit:
2952
if: ${{ !(github.event.pull_request.draft || false) }}
3053
strategy:
3154
fail-fast: false
3255
matrix:
3356
container:
34-
- swift:5.6-focal
35-
- swift:5.7-jammy
36-
- swift:5.8-jammy
37-
- swiftlang/swift:nightly-5.9-jammy
57+
- swift:5.8-focal
58+
- swift:5.9-jammy
59+
- swift:5.10-jammy
60+
- swiftlang/swift:nightly-6.0-jammy
3861
- swiftlang/swift:nightly-main-jammy
3962
redis:
4063
- redis:6
@@ -47,22 +70,11 @@ jobs:
4770
redis-2:
4871
image: ${{ matrix.redis }}
4972
steps:
50-
- name: Save Redis version to env
51-
run: |
52-
echo REDIS_VERSION='${{ matrix.redis }}' >> $GITHUB_ENV
53-
- name: Display versions
54-
shell: bash
55-
run: |
56-
if [[ '${{ contains(matrix.container, 'nightly') }}' == 'true' ]]; then
57-
SWIFT_PLATFORM="$(source /etc/os-release && echo "${ID}${VERSION_ID}")" SWIFT_VERSION="$(cat /.swift_tag)"
58-
printf 'SWIFT_PLATFORM=%s\nSWIFT_VERSION=%s\n' "${SWIFT_PLATFORM}" "${SWIFT_VERSION}" >>"${GITHUB_ENV}"
59-
fi
60-
printf 'OS: %s\nTag: %s\nVersion:\n' "${SWIFT_PLATFORM}-${RUNNER_ARCH}" "${SWIFT_VERSION}" && swift --version
6173
- name: Check out package
62-
uses: actions/checkout@v3
74+
uses: actions/checkout@v4
6375
- name: Run unit tests with Thread Sanitizer and coverage
6476
run: swift test --sanitize=thread --enable-code-coverage
65-
- name: Submit coverage report to Codecov.io
66-
uses: vapor/swift-codecov-action@v0.2
77+
- name: Upload coverage data
78+
uses: vapor/swift-codecov-action@v0.3
6779
with:
68-
cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,REDIS_VERSION'
80+
codecov_token: ${{ secrets.CODECOV_TOKEN }}

Package.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.6
1+
// swift-tools-version:5.8
22
import PackageDescription
33

44
let package = Package(
@@ -14,7 +14,7 @@ let package = Package(
1414
],
1515
dependencies: [
1616
.package(url: "https://github.com/swift-server/RediStack.git", from: "1.4.1"),
17-
.package(url: "https://github.com/vapor/vapor.git", from: "4.77.1"),
17+
.package(url: "https://github.com/vapor/vapor.git", from: "4.100.0"),
1818
],
1919
targets: [
2020
.target(

[email protected]

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// swift-tools-version:5.9
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "redis",
6+
platforms: [
7+
.macOS(.v10_15),
8+
.iOS(.v13),
9+
.tvOS(.v13),
10+
.watchOS(.v6),
11+
],
12+
products: [
13+
.library(name: "Redis", targets: ["Redis"])
14+
],
15+
dependencies: [
16+
.package(url: "https://github.com/swift-server/RediStack.git", from: "1.4.1"),
17+
.package(url: "https://github.com/vapor/vapor.git", from: "4.100.0"),
18+
],
19+
targets: [
20+
.target(
21+
name: "Redis",
22+
dependencies: [
23+
.product(name: "RediStack", package: "RediStack"),
24+
.product(name: "Vapor", package: "vapor"),
25+
],
26+
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
27+
),
28+
.testTarget(
29+
name: "RedisTests",
30+
dependencies: [
31+
.target(name: "Redis"),
32+
.product(name: "XCTVapor", package: "vapor"),
33+
],
34+
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
35+
)
36+
]
37+
)

Sources/Redis/Application.Redis+PubSub.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import Vapor
2-
import RediStack
2+
@preconcurrency import RediStack
33

44
extension Application.Redis {
55
private struct PubSubKey: StorageKey, LockKey {
6-
typealias Value = [RedisID: RedisClient]
6+
typealias Value = [RedisID: RedisClient & Sendable]
77
}
88

99
var pubsubClient: RedisClient {

Sources/Redis/Redis+Cache.swift

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Vapor
22
import Foundation
3-
import RediStack
3+
@preconcurrency import RediStack
44
import NIOCore
55

66
// MARK: RedisCacheCoder
@@ -40,6 +40,11 @@ extension Application.Caches {
4040

4141
/// A cache configured for a given Redis ID and using the provided encoder and decoder.
4242
public func redis<E: RedisCacheEncoder, D: RedisCacheDecoder>(_ id: RedisID = .default, encoder: E, decoder: D) -> Cache {
43+
RedisCache(encoder: FakeSendable(value: encoder), decoder: FakeSendable(value: decoder), client: self.application.redis(id))
44+
}
45+
46+
/// A cache configured for a given Redis ID and using the provided encoder and decoder wrapped as FakeSendable.
47+
func redis(_ id: RedisID = .default, encoder: FakeSendable<some RedisCacheEncoder>, decoder: FakeSendable<some RedisCacheDecoder>) -> Cache {
4348
RedisCache(encoder: encoder, decoder: decoder, client: self.application.redis(id))
4449
}
4550
}
@@ -59,20 +64,25 @@ extension Application.Caches.Provider {
5964

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

6673
// MARK: - Redis cache driver
6774

75+
/// A wrapper to silence `Sendable` warnings for `JSONDecoder` and `JSONEncoder` when not on macOS.
76+
struct FakeSendable<T>: @unchecked Sendable { let value: T }
77+
6878
/// `Cache` driver for storing cache data in Redis, using a provided encoder and decoder to serialize and deserialize values respectively.
69-
private struct RedisCache<CacheEncoder: RedisCacheEncoder, CacheDecoder: RedisCacheDecoder>: Cache {
70-
let encoder: CacheEncoder
71-
let decoder: CacheDecoder
79+
private struct RedisCache<CacheEncoder: RedisCacheEncoder, CacheDecoder: RedisCacheDecoder>: Cache, Sendable {
80+
let encoder: FakeSendable<CacheEncoder>
81+
let decoder: FakeSendable<CacheDecoder>
7282
let client: RedisClient
7383

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

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

8393
return self.client.eventLoop
84-
.tryFuture { try self.encoder.encode(value) }
94+
.tryFuture { try self.encoder.value.encode(value) }
8595
.flatMap {
8696
if let expirationTime = expirationTime {
8797
return self.client.setex(RedisKey(key), to: $0, expirationInSeconds: expirationTime.seconds)

Sources/Redis/Redis+Sessions.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import RediStack
44
import NIOCore
55

66
/// A delegate object that controls key behavior of an `Application.Redis.Sessions` driver.
7-
public protocol RedisSessionsDelegate {
7+
public protocol RedisSessionsDelegate: Sendable {
88
/// Makes a new session ID token.
99
/// - Note: This method is optional to implement.
1010
///

Sources/Redis/RedisConfiguration.swift

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import NIOSSL
33
import NIOPosix
44
import Logging
55
import NIOCore
6-
import RediStack
6+
@preconcurrency import RediStack
77

88
/// Configuration for connecting to a Redis instance
9-
public struct RedisConfiguration {
9+
public struct RedisConfiguration: Sendable {
1010
public typealias ValidationError = RedisConnection.Configuration.ValidationError
1111

1212
public var serverAddresses: [SocketAddress]
@@ -16,21 +16,22 @@ public struct RedisConfiguration {
1616
public var tlsConfiguration: TLSConfiguration?
1717
public var tlsHostname: String?
1818

19-
public struct PoolOptions {
19+
public struct PoolOptions: Sendable {
2020
public var maximumConnectionCount: RedisConnectionPoolSize
2121
public var minimumConnectionCount: Int
2222
public var connectionBackoffFactor: Float32
2323
public var initialConnectionBackoffDelay: TimeAmount
2424
public var connectionRetryTimeout: TimeAmount?
25-
public var onUnexpectedConnectionClose: ((RedisConnection) -> Void)?
25+
public var onUnexpectedConnectionClose: (@Sendable (RedisConnection) -> Void)?
2626

27+
@preconcurrency
2728
public init(
2829
maximumConnectionCount: RedisConnectionPoolSize = .maximumActiveConnections(2),
2930
minimumConnectionCount: Int = 0,
3031
connectionBackoffFactor: Float32 = 2,
3132
initialConnectionBackoffDelay: TimeAmount = .milliseconds(100),
3233
connectionRetryTimeout: TimeAmount? = nil,
33-
onUnexpectedConnectionClose: ((RedisConnection) -> Void)? = nil
34+
onUnexpectedConnectionClose: (@Sendable (RedisConnection) -> Void)? = nil
3435
) {
3536
self.maximumConnectionCount = maximumConnectionCount
3637
self.minimumConnectionCount = minimumConnectionCount

Sources/Redis/RedisID.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public struct RedisID: Hashable,
1616
ExpressibleByStringLiteral,
1717
ExpressibleByStringInterpolation,
1818
CustomStringConvertible,
19-
Comparable {
19+
Comparable,
20+
Sendable {
2021

2122
public let rawValue: String
2223

0 commit comments

Comments
 (0)