Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ let package = Package(
.watchOS(.v6),
],
products: [
.library(name: "Redis", targets: ["Redis"])
.library(name: "Redis", targets: ["Redis"]),
.library(name: "XCTRedis", targets: ["XCTRedis"]),
],
dependencies: [
.package(url: "https://github.com/swift-server/RediStack.git", from: "1.4.1"),
Expand All @@ -24,8 +25,12 @@ let package = Package(
.product(name: "Vapor", package: "vapor"),
]
),
.target(name: "XCTRedis", dependencies: [
.target(name: "Redis"),
]),
.testTarget(name: "RedisTests", dependencies: [
.target(name: "Redis"),
.target(name: "XCTRedis"),
.product(name: "XCTVapor", package: "vapor"),
])
]
Expand Down
21 changes: 15 additions & 6 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import PackageDescription
let package = Package(
name: "redis",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
],
products: [
.library(name: "Redis", targets: ["Redis"])
.library(name: "Redis", targets: ["Redis"]),
.library(name: "XCTRedis", targets: ["XCTRedis"]),
],
dependencies: [
.package(url: "https://github.com/swift-server/RediStack.git", from: "1.4.1"),
Expand All @@ -25,13 +26,21 @@ let package = Package(
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
),
.target(
name: "XCTRedis",
dependencies: [
.target(name: "Redis"),
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
),
.testTarget(
name: "RedisTests",
dependencies: [
.target(name: "Redis"),
.target(name: "XCTRedis"),
.product(name: "XCTVapor", package: "vapor"),
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
)
),
]
)
19 changes: 14 additions & 5 deletions Sources/Redis/Application+Redis.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ extension Application {
}

@usableFromInline
internal func pool(for eventLoop: EventLoop) -> RedisConnectionPool {
internal func pool(for eventLoop: EventLoop) -> RedisClient {
self.application.redisStorage.pool(for: eventLoop, id: self.id)
}

public func use(_ configuration: RedisConfigurationFactory) {
self.application.redisStorage.use(configuration, as: id)
}
}
}

Expand Down Expand Up @@ -89,10 +93,15 @@ extension Application.Redis {
public func withBorrowedConnection<Result>(
_ operation: @escaping (RedisClient) -> EventLoopFuture<Result>
) -> EventLoopFuture<Result> {
return self.application.redis(self.id)
let client = self.application.redis(self.id)
.pool(for: self.eventLoop)
.leaseConnection {
return operation($0.logging(to: self.application.logger))
}

guard let pool = client as? RedisConnectionPool else {
return self.eventLoop.makeFailedFuture(Application.Redis.Errors.unsupportedOperation)
}

return pool.leaseConnection {
return operation($0.logging(to: self.application.logger))
}
}
}
18 changes: 0 additions & 18 deletions Sources/Redis/Application.Redis+configuration.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation
import Logging
import NIOCore
import NIOSSL

/// A protocol which indicates the ability to create a ``RedisClient``
public protocol RedisFactory: Sendable {
/// Configuration on which ``RedisFactory/makeClient(for:logger:)`` is based
var configuration: RedisConfiguration { get }

/// A method that generates a ``RediStack/RedisClient``
/// - Parameters:
/// - eventLoop: indicates the eventLoop on which the client will execute their commands.
/// - logger: indicates a specific logger associated with the client
/// - Returns: a ``RediStack/RedisClient``
func makeClient(for eventLoop: EventLoop, logger: Logger) -> RedisClient
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import NIOCore
import NIOSSL

extension RedisConfigurationFactory {
public static func standalone(
url string: String,
tlsConfiguration: TLSConfiguration? = nil,
pool: RedisConfiguration.PoolOptions = .init(),
logger: Logger? = nil
) throws -> Self {
guard let url = URL(string: string) else { throw ValidationError.invalidURLString }
return try standalone(url: url, tlsConfiguration: tlsConfiguration, pool: pool, logger: logger)
}

public static func standalone(
url: URL,
tlsConfiguration: TLSConfiguration? = nil,
pool: RedisConfiguration.PoolOptions = .init(),
logger: Logger? = nil
) throws -> Self {
guard
let scheme = url.scheme,
!scheme.isEmpty
else { throw ValidationError.missingURLScheme }

guard scheme == "redis" || scheme == "rediss" else { throw ValidationError.invalidURLScheme }
guard let host = url.host, !host.isEmpty else { throw ValidationError.missingURLHost }

let defaultTLSConfig: TLSConfiguration?
if scheme == "rediss" {
// If we're given a 'rediss' URL, make sure we have at least a default TLS config.
defaultTLSConfig = tlsConfiguration ?? .makeClientConfiguration()
} else {
defaultTLSConfig = tlsConfiguration
}

return try standalone(
hostname: host,
port: url.port ?? RedisConnection.Configuration.defaultPort,
password: url.password,
tlsConfiguration: defaultTLSConfig,
database: Int(url.lastPathComponent),
pool: pool,
logger: logger
)
}

public static func standalone(
hostname: String,
port: Int = RedisConnection.Configuration.defaultPort,
password: String? = nil,
tlsConfiguration: TLSConfiguration? = nil,
database: Int? = nil,
pool: RedisConfiguration.PoolOptions = .init(),
logger: Logger? = nil
) throws -> Self {
if let database, database < 0 {
throw ValidationError.outOfBoundsDatabaseID
}

return try standalone(
serverAddresses: [.makeAddressResolvingHost(hostname, port: port)],
password: password,
tlsConfiguration: tlsConfiguration,
tlsHostname: hostname,
database: database,
pool: pool,
logger: logger
)
}

public static func standalone(
serverAddresses: [SocketAddress],
password: String? = nil,
tlsConfiguration: TLSConfiguration? = nil,
tlsHostname: String? = nil,
database: Int? = nil,
pool: RedisConfiguration.PoolOptions = .init(),
logger: Logger? = nil
) throws -> Self {
.init {
RedisConfiguration(
serverAddresses: serverAddresses,
password: password,
database: database,
pool: pool,
tlsConfiguration: tlsConfiguration,
tlsHostname: tlsHostname,
logger: logger
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

public struct RedisConfigurationFactory: Sendable {
typealias ValidationError = RedisConfiguration.ValidationError

public let make: @Sendable () -> RedisFactory

public init(make: @escaping @Sendable () -> RedisFactory) {
self.make = make
}
}
40 changes: 40 additions & 0 deletions Sources/Redis/Configuration/RedisConfiguration+Factory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import NIOCore
import NIOPosix
import NIOSSL
import RediStack
import Vapor

extension RedisConfiguration: RedisFactory {
public var configuration: RedisConfiguration { self }

public func makeClient(for eventLoop: EventLoop, logger: Logger) -> RedisClient {
let redisTLSClient: ClientBootstrap? = {
guard let tlsConfig = self.tlsConfiguration,
let tlsHost = self.tlsHostname else { return nil }

return ClientBootstrap(group: eventLoop)
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.channelInitializer { channel in
do {
let sslContext = try NIOSSLContext(configuration: tlsConfig)
return EventLoopFuture.andAllSucceed([
channel.pipeline.addHandler(
try NIOSSLClientHandler(
context: sslContext,
serverHostname: tlsHost
)
),
channel.pipeline.addBaseRedisHandlers(),
], on: channel.eventLoop)
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}
}()

return RedisConnectionPool(
configuration: .init(self, defaultLogger: self.logger ?? logger, customClient: redisTLSClient),
boundEventLoop: eventLoop
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation
@preconcurrency import RediStack

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

public init(
maximumConnectionCount: RedisConnectionPoolSize = .maximumActiveConnections(2),
minimumConnectionCount: Int = 0,
connectionBackoffFactor: Float32 = 2,
initialConnectionBackoffDelay: TimeAmount = .milliseconds(100),
connectionRetryTimeout: TimeAmount? = nil,
onUnexpectedConnectionClose: (@Sendable (RedisConnection) -> Void)? = nil
) {
self.maximumConnectionCount = maximumConnectionCount
self.minimumConnectionCount = minimumConnectionCount
self.connectionBackoffFactor = connectionBackoffFactor
self.initialConnectionBackoffDelay = initialConnectionBackoffDelay
self.connectionRetryTimeout = connectionRetryTimeout
self.onUnexpectedConnectionClose = onUnexpectedConnectionClose
}
}
}
27 changes: 27 additions & 0 deletions Sources/Redis/Configuration/RedisConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation
import Logging
import NIOCore
import NIOSSL

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

public let serverAddresses: [SocketAddress]
public let password: String?
public let database: Int?
public let pool: PoolOptions
public let tlsConfiguration: TLSConfiguration?
public let tlsHostname: String?
public let logger: Logger?

init(serverAddresses: [SocketAddress], password: String?, database: Int?, pool: PoolOptions, tlsConfiguration: TLSConfiguration?, tlsHostname: String?, logger: Logger?) {
self.serverAddresses = serverAddresses
self.password = password
self.database = database
self.pool = pool
self.tlsConfiguration = tlsConfiguration
self.tlsHostname = tlsHostname
self.logger = logger
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import NIOPosix
import RediStack

extension RedisConnectionPool.Configuration {
internal init(_ config: RedisConfiguration, defaultLogger: Logger, customClient: ClientBootstrap?) {
self.init(
initialServerConnectionAddresses: config.serverAddresses,
maximumConnectionCount: config.pool.maximumConnectionCount,
connectionFactoryConfiguration: .init(
connectionInitialDatabase: config.database,
connectionPassword: config.password,
connectionDefaultLogger: defaultLogger,
tcpClient: customClient
),
minimumConnectionCount: config.pool.minimumConnectionCount,
connectionBackoffFactor: config.pool.connectionBackoffFactor,
initialConnectionBackoffDelay: config.pool.initialConnectionBackoffDelay,
connectionRetryTimeout: config.pool.connectionRetryTimeout,
onUnexpectedConnectionClose: config.pool.onUnexpectedConnectionClose,
poolDefaultLogger: defaultLogger
)
}
}
14 changes: 14 additions & 0 deletions Sources/Redis/Redis+Errors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Vapor

public extension Application.Redis {
enum Errors: Error {
case unsupportedOperation

var localizedDescription: String {
switch self {
case .unsupportedOperation:
return "Underlying client is not a RedisConnectionPool."
}
}
}
}
Loading