Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Async Lifecycles #214

Merged
merged 10 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/[email protected]
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL's still broken, pull this back out


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 [email protected]
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
Loading