mirror of
https://github.com/swift-server/RediStack.git
synced 2026-05-03 07:32:28 +00:00
81 -- Add Configuration types for initialization
This commit is contained in:
@@ -60,9 +60,9 @@ import NIO
|
||||
import RediStack
|
||||
|
||||
let eventLoop: EventLoop = ...
|
||||
let connection = RedisConnection.connect(
|
||||
to: try .init(ipAddress: "127.0.0.1", port: RedisConnection.defaultPort),
|
||||
on: eventLoop
|
||||
let connection = RedisConnection.make(
|
||||
configuration: try .init(hostname: "127.0.0.1"),
|
||||
boundEventLoop: eventLoop
|
||||
).wait()
|
||||
|
||||
let result = try connection.set("my_key", to: "some value")
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the RediStack open source project
|
||||
//
|
||||
// Copyright (c) 2020 RediStack project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of RediStack project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Logging
|
||||
import NIO
|
||||
|
||||
extension RedisConnection.Configuration {
|
||||
public struct ValidationError: LocalizedError, Equatable {
|
||||
public static let invalidURLString = ValidationError(.invalidURLString)
|
||||
public static let missingURLScheme = ValidationError(.missingURLScheme)
|
||||
public static let invalidURLScheme = ValidationError(.invalidURLScheme)
|
||||
public static let missingURLHost = ValidationError(.missingURLHost)
|
||||
public static let outOfBoundsDatabaseID = ValidationError(.outOfBoundsDatabaseID)
|
||||
|
||||
var localizedDescription: String { self.kind.localizedDescription }
|
||||
|
||||
private let kind: Kind
|
||||
|
||||
private init(_ kind: Kind) { self.kind = kind }
|
||||
|
||||
public static func ==(lhs: ValidationError, rhs: ValidationError) -> Bool {
|
||||
return lhs.kind == rhs.kind
|
||||
}
|
||||
|
||||
private enum Kind: LocalizedError {
|
||||
case invalidURLString
|
||||
case missingURLScheme
|
||||
case invalidURLScheme
|
||||
case missingURLHost
|
||||
case outOfBoundsDatabaseID
|
||||
|
||||
var localizedDescription: String {
|
||||
let message: String = {
|
||||
switch self {
|
||||
case .invalidURLString: return "invalid URL string"
|
||||
case .missingURLScheme: return "URL scheme is missing"
|
||||
case .invalidURLScheme: return "invalid URL scheme, expected 'redis'"
|
||||
case .missingURLHost: return "missing remote hostname"
|
||||
case .outOfBoundsDatabaseID: return "database index out of bounds"
|
||||
}
|
||||
}()
|
||||
return "(RediStack) \(RedisConnection.Configuration.self) validation failed: \(message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RedisConnection Config
|
||||
|
||||
extension RedisConnection {
|
||||
/// A configuration object for creating a single connection to Redis.
|
||||
public struct Configuration {
|
||||
/// The default port that Redis uses.
|
||||
///
|
||||
/// See [https://redis.io/topics/quickstart](https://redis.io/topics/quickstart)
|
||||
public static var defaultPort = 6379
|
||||
|
||||
internal static let defaultLogger = Logger.redisBaseConnectionLogger
|
||||
|
||||
/// The hostname of the connection address. If the address is a Unix socket, then it will be `nil`.
|
||||
public var hostname: String? {
|
||||
switch self.address {
|
||||
case let .v4(addr): return addr.host
|
||||
case let .v6(addr): return addr.host
|
||||
case .unixDomainSocket: return nil
|
||||
}
|
||||
}
|
||||
/// The port of the connection address. If the address is a Unix socket, then it will be `nil`.
|
||||
public var port: Int? { self.address.port }
|
||||
/// The password used to authenticate the connection.
|
||||
public let password: String?
|
||||
/// The initial database index that the connection should use.
|
||||
public let initialDatabase: Int?
|
||||
/// The logger prototype that will be used by the connection by default when generating logs.
|
||||
public let defaultLogger: Logger
|
||||
|
||||
internal let address: SocketAddress
|
||||
|
||||
/// Creates a new connection configuration with the provided details.
|
||||
/// - Parameters:
|
||||
/// - address: The socket address information to use for creating the Redis connection.
|
||||
/// - password: The optional password to authenticate the connection with. The default is `nil`.
|
||||
/// - initialDatabase: The optional database index to initially connect to. The default is `nil`.
|
||||
/// Redis by default opens connections against index `0`, so only set this value if the desired default is not `0`.
|
||||
/// - defaultLogger: The optional prototype logger to use as the default logger instance when generating logs from the connection.
|
||||
/// If one is not provided, one will be generated. See `RedisLogging.baseConnectionLogger`.
|
||||
/// - Throws: `RedisConnection.Configuration.ValidationError` if invalid arguments are provided.
|
||||
public init(
|
||||
address: SocketAddress,
|
||||
password: String? = nil,
|
||||
initialDatabase: Int? = nil,
|
||||
defaultLogger: Logger? = nil
|
||||
) throws {
|
||||
if initialDatabase != nil && initialDatabase! < 0 {
|
||||
throw ValidationError.outOfBoundsDatabaseID
|
||||
}
|
||||
|
||||
self.address = address
|
||||
self.password = password
|
||||
self.initialDatabase = initialDatabase
|
||||
self.defaultLogger = defaultLogger ?? Configuration.defaultLogger
|
||||
}
|
||||
|
||||
/// Creates a new connection configuration with exact details.
|
||||
/// - Parameters:
|
||||
/// - hostname: The remote hostname to connect to.
|
||||
/// - port: The port that the Redis instance connects with. The default is `RedisConnection.Configuration.defaultPort`.
|
||||
/// - password: The optional password to authenticate the connection with. The default is `nil`.
|
||||
/// - initialDatabase: The optional database index to initially connect to. The default is `nil`.
|
||||
/// Redis by default opens connections against index `0`, so only set this value if the desired default is not `0`.
|
||||
/// - defaultLogger: The optional prototype logger to use as the default logger instance when generating logs from the connection.
|
||||
/// If one is not provided, one will be generated. See `RedisLogging.baseConnectionLogger`.
|
||||
/// - Throws:
|
||||
/// - `NIO.SocketAddressError` if hostname resolution fails.
|
||||
/// - `RedisConnection.Configuration.ValidationError` if invalid arguments are provided.
|
||||
public init(
|
||||
hostname: String,
|
||||
port: Int = Self.defaultPort,
|
||||
password: String? = nil,
|
||||
initialDatabase: Int? = nil,
|
||||
defaultLogger: Logger? = nil
|
||||
) throws {
|
||||
try self.init(
|
||||
address: try .makeAddressResolvingHost(hostname, port: port),
|
||||
password: password,
|
||||
initialDatabase: initialDatabase,
|
||||
defaultLogger: defaultLogger
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new connection configuration from the provided RFC 1808 URL formatted string.
|
||||
///
|
||||
/// This is a convenience initializer over creating a `Foundation.URL` directly and passing it to the overloaded initializer.
|
||||
///
|
||||
/// The string is expected to match the [RFC 1808](https://tools.ietf.org/html/rfc1808) format.
|
||||
///
|
||||
/// An example string:
|
||||
///
|
||||
/// redis://:password@localhost:6379/3
|
||||
///
|
||||
/// For more details, see the `Configuration.init(url:)` overload that accepts a `Foundation.URL`.
|
||||
/// - Parameters:
|
||||
/// - string: The URL formatted string.
|
||||
/// - defaultLogger: The optional prototype logger to use as the default logger instance when generating logs from the connection.
|
||||
/// If one is not provided, one will be generated. See `RedisLogging.baseConnectionLogger`.
|
||||
/// - Throws:
|
||||
/// - `RedisConnection.Configuration.ValidationError` if required URL components are invalid or missing.
|
||||
/// - `NIO.SocketAddressError` if hostname resolution fails.
|
||||
public init(url string: String, defaultLogger: Logger? = nil) throws {
|
||||
guard let url = URL(string: string) else { throw ValidationError.invalidURLString }
|
||||
try self.init(url: url, defaultLogger: defaultLogger)
|
||||
}
|
||||
|
||||
/// Creates a new connection configuration from the provided URL object.
|
||||
///
|
||||
/// The `scheme` and `host` are the only properties that need to be established.
|
||||
/// - Invariant: The `port` property is optional, with the `RedisConnection.Configuration.defaultPort` being used by default.
|
||||
/// - Invariant: `password` is only required if the Redis instance specifies a password is required. This will not be detected until trying to establish a connection
|
||||
/// with this configuration.
|
||||
/// - Invariant: To set the default selected database index, provide the index as the `lastPathComponent` of the `Foundation.URL`.
|
||||
/// - Requires: The URL **must** use the `redis://` scheme.
|
||||
/// - Parameters:
|
||||
/// - url: The URL to use to resolve and authenticate the remote connection.
|
||||
/// - defaultLogger: The optional prototype logger to use as the default logger instance when generating logs from the connection.
|
||||
/// If one is not provided, one will be generated. See `RedisLogging.baseConnectionLogger`.
|
||||
/// - Throws:
|
||||
/// - `RedisConnection.Configuration.ValidationError` if required URL components are invalid or missing.
|
||||
/// - `NIO.SocketAddressError` if hostname resolution fails.
|
||||
public init(url: URL, defaultLogger: Logger? = nil) throws {
|
||||
try Self.validateRedisURL(url)
|
||||
|
||||
guard let host = url.host, !host.isEmpty else { throw ValidationError.missingURLHost }
|
||||
|
||||
let databaseID = Int(url.lastPathComponent)
|
||||
|
||||
try self.init(
|
||||
address: try .makeAddressResolvingHost(host, port: url.port ?? Self.defaultPort),
|
||||
password: url.password,
|
||||
initialDatabase: databaseID,
|
||||
defaultLogger: defaultLogger
|
||||
)
|
||||
}
|
||||
|
||||
private static func validateRedisURL(_ url: URL) throws {
|
||||
guard
|
||||
let scheme = url.scheme,
|
||||
!scheme.isEmpty
|
||||
else { throw ValidationError.missingURLScheme }
|
||||
|
||||
guard scheme == "redis" else { throw ValidationError.invalidURLScheme }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RedisConnectionPool Config
|
||||
|
||||
extension RedisConnectionPool {
|
||||
/// A configuration object for creating Redis connections with a connection pool.
|
||||
/// - Warning: This type has **reference** semantics due to the `NIO.ClientBootstrap` reference.
|
||||
public struct ConnectionFactoryConfiguration {
|
||||
// this needs to be var so it can be updated by the pool with the pool id
|
||||
/// The logger prototype that will be used by connections by default when generating logs.
|
||||
public internal(set) var connectionDefaultLogger: Logger
|
||||
/// The password used to authenticate connections.
|
||||
public let connectionPassword: String?
|
||||
/// The initial database index that connections should use.
|
||||
public let connectionInitialDatabase: Int?
|
||||
/// The pre-configured TCP client for connections to use.
|
||||
public let tcpClient: ClientBootstrap?
|
||||
|
||||
/// Creates a new connection factory configuration with the provided options.
|
||||
/// - Parameters:
|
||||
/// - connectionInitialDatabase: The optional database index to initially connect to. The default is `nil`.
|
||||
/// Redis by default opens connections against index `0`, so only set this value if the desired default is not `0`.
|
||||
/// - connectionPassword: The optional password to authenticate connections with. The default is `nil`.
|
||||
/// - connectionDefaultLogger: The optional prototype logger to use as the default logger instance when generating logs from connections.
|
||||
/// If one is not provided, one will be generated. See `RedisLogging.baseConnectionLogger`.
|
||||
/// - tcpClient: If you have chosen to configure a `NIO.ClientBootstrap` yourself, this will be used instead of the `.makeRedisTCPClient` factory instance.
|
||||
public init(
|
||||
connectionInitialDatabase: Int? = nil,
|
||||
connectionPassword: String? = nil,
|
||||
connectionDefaultLogger: Logger? = nil,
|
||||
tcpClient: ClientBootstrap? = nil
|
||||
) {
|
||||
self.connectionInitialDatabase = connectionInitialDatabase
|
||||
self.connectionPassword = connectionPassword
|
||||
self.connectionDefaultLogger = connectionDefaultLogger ?? RedisConnection.Configuration.defaultLogger
|
||||
self.tcpClient = tcpClient
|
||||
}
|
||||
}
|
||||
|
||||
/// A configuration object for connection pools.
|
||||
/// - Warning: This type has **reference** semantics due to `ConnectionFactoryConfiguration`.
|
||||
public struct Configuration {
|
||||
/// The set of Redis servers to which this pool is initially willing to connect.
|
||||
public let initialConnectionAddresses: [SocketAddress]
|
||||
/// The minimum number of connections to preserve in the pool.
|
||||
///
|
||||
/// If the pool is mostly idle and the Redis servers close these idle connections,
|
||||
/// the `RedisConnectionPool` will initiate new outbound connections proactively to avoid the number of available connections dropping below this number.
|
||||
public let minimumConnectionCount: Int
|
||||
/// The maximum number of connections to for this pool, either to be preserved or as a hard limit.
|
||||
public let maximumConnectionCount: RedisConnectionPoolSize
|
||||
/// The configuration object that controls the connection retry behavior.
|
||||
public let connectionRetryConfiguration: (backoff: (initialDelay: TimeAmount, factor: Float32), timeout: TimeAmount)
|
||||
// these need to be var so they can be updated by the pool in some cases
|
||||
public internal(set) var factoryConfiguration: ConnectionFactoryConfiguration
|
||||
/// The logger prototype that will be used by the connection pool by default when generating logs.
|
||||
public internal(set) var poolDefaultLogger: Logger
|
||||
|
||||
/// Creates a new connection configuration with the provided options.
|
||||
/// - Parameters:
|
||||
/// - initialServerConnectionAddresses: The set of Redis servers to which this pool is initially willing to connect.
|
||||
/// This set can be updated over time directly on the connection pool.
|
||||
/// - maximumConnectionCount: The maximum number of connections to for this pool, either to be preserved or as a hard limit.
|
||||
/// - connectionFactoryConfiguration: The configuration to use while creating connections to fill the pool.
|
||||
/// - minimumConnectionCount: The minimum number of connections to preserve in the pool. If the pool is mostly idle
|
||||
/// and the Redis servers close these idle connections, the `RedisConnectionPool` will initiate new outbound
|
||||
/// connections proactively to avoid the number of available connections dropping below this number. Defaults to `1`.
|
||||
/// - connectionBackoffFactor: Used when connection attempts fail to control the exponential backoff. This is a multiplicative
|
||||
/// factor, each connection attempt will be delayed by this amount times the previous delay.
|
||||
/// - initialConnectionBackoffDelay: If a TCP connection attempt fails, this is the first backoff value on the reconnection attempt.
|
||||
/// Subsequent backoffs are computed by compounding this value by `connectionBackoffFactor`.
|
||||
/// - connectionRetryTimeout: The max time to wait for a connection to be available before failing a particular command or connection operation.
|
||||
/// The default is 60 seconds.
|
||||
/// - poolDefaultLogger: The `Logger` used by the connection pool itself.
|
||||
public init(
|
||||
initialServerConnectionAddresses: [SocketAddress],
|
||||
maximumConnectionCount: RedisConnectionPoolSize,
|
||||
connectionFactoryConfiguration: ConnectionFactoryConfiguration,
|
||||
minimumConnectionCount: Int = 1,
|
||||
connectionBackoffFactor: Float32 = 2,
|
||||
initialConnectionBackoffDelay: TimeAmount = .milliseconds(100),
|
||||
connectionRetryTimeout: TimeAmount? = .seconds(60),
|
||||
poolDefaultLogger: Logger? = nil
|
||||
) {
|
||||
self.initialConnectionAddresses = initialServerConnectionAddresses
|
||||
self.maximumConnectionCount = maximumConnectionCount
|
||||
self.factoryConfiguration = connectionFactoryConfiguration
|
||||
self.minimumConnectionCount = minimumConnectionCount
|
||||
self.connectionRetryConfiguration = (
|
||||
(initialConnectionBackoffDelay, connectionBackoffFactor),
|
||||
connectionRetryTimeout ?? .milliseconds(10) // always default to a baseline 10ms
|
||||
)
|
||||
self.poolDefaultLogger = poolDefaultLogger ?? .redisBaseConnectionPoolLogger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `RedisConnectionPoolSize` controls how the maximum number of connections in a pool are interpreted.
|
||||
public enum RedisConnectionPoolSize {
|
||||
/// The pool will allow no more than this number of connections to be "active" (that is, connecting, in-use,
|
||||
/// or pooled) at any one time. This will force possible future users of new connections to wait until a currently
|
||||
/// active connection becomes available by being returned to the pool, but provides a hard upper limit on concurrency.
|
||||
case maximumActiveConnections(Int)
|
||||
|
||||
/// The pool will only store up to this number of connections that are not currently in-use. However, if the pool is
|
||||
/// asked for more connections at one time than this number, it will create new connections to serve those waiting for
|
||||
/// connections. These "extra" connections will not be preserved: while they will be used to satisfy those waiting for new
|
||||
/// connections if needed, they will not be preserved in the pool if load drops low enough. This does not provide a hard
|
||||
/// upper bound on concurrency, but does provide an upper bound on low-level load.
|
||||
case maximumPreservedConnections(Int)
|
||||
}
|
||||
@@ -20,25 +20,20 @@ import NIO
|
||||
import NIOConcurrencyHelpers
|
||||
|
||||
extension RedisConnection {
|
||||
/// The documented default port that Redis connects through.
|
||||
///
|
||||
/// See [https://redis.io/topics/quickstart](https://redis.io/topics/quickstart)
|
||||
public static let defaultPort = 6379
|
||||
|
||||
/// Creates a new connection to a Redis instance.
|
||||
/// Creates a new connection with provided configuration and sychronization objects.
|
||||
///
|
||||
/// If you would like to specialize the `NIO.ClientBootstrap` that the connection communicates on, override the default by passing it in as `tcpClient`.
|
||||
/// If you would like to specialize the `NIO.ClientBootstrap` that the connection communicates on, override the default by passing it in as `configuredTCPClient`.
|
||||
///
|
||||
/// let eventLoopGroup: EventLoopGroup = ...
|
||||
/// var customTCPClient = ClientBootstrap.makeRedisTCPClient(group: eventLoopGroup)
|
||||
/// customTCPClient.channelInitializer { channel in
|
||||
/// // channel customizations
|
||||
/// }
|
||||
/// let connection = RedisConnection.connect(
|
||||
/// to: ...,
|
||||
/// on: eventLoopGroup.next(),
|
||||
/// password: ...,
|
||||
/// tcpClient: customTCPClient
|
||||
/// let connection = RedisConnection.make(
|
||||
/// configuration: ...,
|
||||
/// boundEventLoop: eventLoopGroup.next(),
|
||||
/// configuredTCPClient: customTCPClient
|
||||
/// ).wait()
|
||||
///
|
||||
/// It is recommended that you be familiar with `ClientBootstrap.makeRedisTCPClient(group:)` and `NIO.ClientBootstrap` in general before doing so.
|
||||
@@ -46,45 +41,50 @@ extension RedisConnection {
|
||||
/// Note: Use of `wait()` in the example is for simplicity. Never call `wait()` on an event loop.
|
||||
///
|
||||
/// - Important: Call `close()` on the connection before letting the instance deinit to properly cleanup resources.
|
||||
/// - Note: If a `password` is provided, the connection will send an "AUTH" command to Redis as soon as it has been opened.
|
||||
///
|
||||
/// - Invariant: If a `password` is provided in the configuration, the connection will send an "AUTH" command to Redis as soon as it has been opened.
|
||||
/// - Invariant: If a `database` index is provided in the configuration, the connection will send a "SELECT" command to Redis after it has been authenticated.
|
||||
/// - Parameters:
|
||||
/// - socket: The `NIO.SocketAddress` information of the Redis instance to connect to.
|
||||
/// - eventLoop: The `NIO.EventLoop` that this connection will execute all tasks on.
|
||||
/// - password: The optional password to use for authorizing the connection with Redis.
|
||||
/// - logger: The `Logging.Logger` instance to use for all client logging purposes. If one is not provided, one will be created.
|
||||
/// A `Foundation.UUID` will be attached to the metadata to uniquely identify this connection instance's logs.
|
||||
/// - tcpClient: If you have chosen to configure a `NIO.ClientBootstrap` yourself, this will be used instead of the `makeRedisTCPClient` instance.
|
||||
/// - Returns: A `NIO.EventLoopFuture` that resolves with the new connection after it has been opened, and if a `password` is provided, authenticated.
|
||||
public static func connect(
|
||||
to socket: SocketAddress,
|
||||
on eventLoop: EventLoop,
|
||||
password: String? = nil,
|
||||
logger: Logger = .redisBaseConnectionLogger,
|
||||
tcpClient: ClientBootstrap? = nil
|
||||
/// - config: The configuration to use for creating the connection.
|
||||
/// - eventLoop: The `NIO.EventLoop` that the connection will be bound to.
|
||||
/// - client: If you have chosen to configure a `NIO.ClientBootstrap` yourself, this will be used instead of the `.makeRedisTCPClient` factory instance.
|
||||
/// - Returns: A `NIO.EventLoopFuture` that resolves with the new connection after it has been opened, configured, and authenticated per the `configuration` object.
|
||||
public static func make(
|
||||
configuration config: Configuration,
|
||||
boundEventLoop eventLoop: EventLoop,
|
||||
configuredTCPClient client: ClientBootstrap? = nil
|
||||
) -> EventLoopFuture<RedisConnection> {
|
||||
let client = tcpClient ?? ClientBootstrap.makeRedisTCPClient(group: eventLoop)
|
||||
let client = client ?? .makeRedisTCPClient(group: eventLoop)
|
||||
|
||||
return client.connect(to: socket)
|
||||
.map { return RedisConnection(configuredRESPChannel: $0, context: logger) }
|
||||
.flatMap { connection in
|
||||
guard let pw = password else {
|
||||
return connection.eventLoop.makeSucceededFuture(connection)
|
||||
}
|
||||
return connection.authorize(with: pw)
|
||||
.map { return connection }
|
||||
var future = client
|
||||
.connect(to: config.address)
|
||||
.map { return RedisConnection(configuredRESPChannel: $0, context: config.defaultLogger) }
|
||||
|
||||
// if a password is specified, use it to authenticate before further operations happen
|
||||
if let password = config.password {
|
||||
future = future.flatMap { connection in
|
||||
return connection.authorize(with: password).map { connection }
|
||||
}
|
||||
}
|
||||
|
||||
// if a database index is specified, use it to switch the selected database before further operations happen
|
||||
if let database = config.initialDatabase {
|
||||
future = future.flatMap { connection in
|
||||
return connection.select(database: database).map { connection }
|
||||
}
|
||||
}
|
||||
|
||||
return future
|
||||
}
|
||||
}
|
||||
|
||||
/// A concrete `RedisClient` implementation that represents an individual connection to a Redis database instance.
|
||||
/// A concrete `RedisClient` implementation that represents an individual connection to a Redis instance.
|
||||
///
|
||||
/// For basic setups, you will just need a `NIO.SocketAddress` and a `NIO.EventLoop` and perhaps a `password`.
|
||||
/// For basic setups, you will just need a `NIO.EventLoop` and perhaps a `password`.
|
||||
///
|
||||
/// let eventLoop: EventLoop = ...
|
||||
/// let connection = RedisConnection.connect(
|
||||
/// to: try .makeAddressResolvingHost("my.redis.url", port: RedisConnection.defaultPort),
|
||||
/// on: eventLoop
|
||||
/// let connection = RedisConnection.make(
|
||||
/// configuration: .init(hostname: "my.redis.url", password: "some_password"),
|
||||
/// boundEventLoop: eventLoop
|
||||
/// ).wait()
|
||||
///
|
||||
/// let result = try connection.set("my_key", to: "some value")
|
||||
@@ -94,8 +94,6 @@ extension RedisConnection {
|
||||
/// print(result) // Optional("some value")
|
||||
///
|
||||
/// Note: `wait()` is used in the example for simplicity. Never call `wait()` on an event loop.
|
||||
///
|
||||
/// See `NIO.SocketAddress`, `NIO.EventLoop`, and `RedisClient`.
|
||||
public final class RedisConnection: RedisClient, RedisClientWithUserContext {
|
||||
/// A unique identifer to represent this connection.
|
||||
public let id = UUID()
|
||||
|
||||
@@ -34,83 +34,47 @@ public class RedisConnectionPool {
|
||||
/// The number of connections that have been handed out and are in active use.
|
||||
public var leasedConnectionCount: Int { self.pool?.leasedConnectionCount ?? 0 }
|
||||
|
||||
private let loop: EventLoop
|
||||
// This needs to be var because we hand it a closure that references us strongly. This also
|
||||
// establishes a reference cycle which we need to break.
|
||||
// Aside from on init, all other operations on this var must occur on the event loop.
|
||||
private var pool: ConnectionPool?
|
||||
|
||||
/// This needs to be var because it is updatable and mutable. As a result, aside from init,
|
||||
/// all use of this var must occur on the event loop.
|
||||
// This needs to be var because of some logger metadata tagging.
|
||||
// This should only be mutated on init, and safe to read anywhere else
|
||||
private var configuration: Configuration
|
||||
// This needs to be var because it is updatable and mutable.
|
||||
// As a result, aside from init, all use of this var must occur on the event loop.
|
||||
private var serverConnectionAddresses: ConnectionAddresses
|
||||
/// This needs to be a var because we reuse the same connection
|
||||
// This needs to be a var because its value changes as the pool enters/leaves pubsub mode to reuse the same connection.
|
||||
private var pubsubConnection: RedisConnection?
|
||||
|
||||
public init(configuration: Configuration, boundEventLoop: EventLoop) {
|
||||
var config = configuration
|
||||
|
||||
private let connectionRetryTimeout: TimeAmount
|
||||
private let connectionPassword: String?
|
||||
private let connectionSystemContext: Logger
|
||||
private let poolSystemContext: Context
|
||||
private let loop: EventLoop
|
||||
private let connectionTCPClient: ClientBootstrap?
|
||||
|
||||
/// Create a new `RedisConnectionPool`.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - serverConnectionAddresses: The set of Redis servers to which this pool is initially willing to connect.
|
||||
/// This set can be updated over time.
|
||||
/// - loop: The event loop to which this pooled client is tied.
|
||||
/// - maximumConnectionCount: The maximum number of connections to for this pool, either to be preserved or as a hard limit.
|
||||
/// - minimumConnectionCount: The minimum number of connections to preserve in the pool. If the pool is mostly idle
|
||||
/// and the Redis servers close these idle connections, the `RedisConnectionPool` will initiate new outbound
|
||||
/// connections proactively to avoid the number of available connections dropping below this number. Defaults to `1`.
|
||||
/// - connectionPassword: The password to use to connect to the Redis servers in this pool.
|
||||
/// - connectionLogger: The `Logger` to pass to each connection in the pool.
|
||||
/// - connectionTCPClient: The base `ClientBootstrap` to use to create pool connections, if a custom one is in use.
|
||||
/// - poolLogger: The `Logger` used by the connection pool itself.
|
||||
/// - connectionBackoffFactor: Used when connection attempts fail to control the exponential backoff. This is a multiplicative
|
||||
/// factor, each connection attempt will be delayed by this amount times the previous delay.
|
||||
/// - initialConnectionBackoffDelay: If a TCP connection attempt fails, this is the first backoff value on the reconnection attempt.
|
||||
/// Subsequent backoffs are computed by compounding this value by `connectionBackoffFactor`.
|
||||
/// - connectionRetryTimeout: The max time to wait for a connection to be available before failing a particular command or connection operation.
|
||||
/// The default is 60 seconds.
|
||||
public init(
|
||||
serverConnectionAddresses: [SocketAddress],
|
||||
loop: EventLoop,
|
||||
maximumConnectionCount: RedisConnectionPoolSize,
|
||||
minimumConnectionCount: Int = 1,
|
||||
connectionPassword: String? = nil,
|
||||
connectionLogger: Logger = .redisBaseConnectionLogger,
|
||||
connectionTCPClient: ClientBootstrap? = nil,
|
||||
poolLogger: Logger = .redisBaseConnectionPoolLogger,
|
||||
connectionBackoffFactor: Float32 = 2,
|
||||
initialConnectionBackoffDelay: TimeAmount = .milliseconds(100),
|
||||
connectionRetryTimeout: TimeAmount? = .seconds(60)
|
||||
) {
|
||||
self.loop = loop
|
||||
self.serverConnectionAddresses = ConnectionAddresses(initialAddresses: serverConnectionAddresses)
|
||||
self.connectionPassword = connectionPassword
|
||||
self.connectionRetryTimeout = connectionRetryTimeout ?? .milliseconds(10)
|
||||
self.loop = boundEventLoop
|
||||
self.serverConnectionAddresses = ConnectionAddresses(initialAddresses: config.initialConnectionAddresses)
|
||||
|
||||
// mix of terminology here with the loggers
|
||||
// as we're being "forward thinking" in terms of the 'baggage context' future type
|
||||
|
||||
var connectionLogger = connectionLogger
|
||||
connectionLogger[metadataKey: RedisLogging.MetadataKeys.connectionPoolID] = "\(self.id)"
|
||||
self.connectionSystemContext = connectionLogger
|
||||
|
||||
var poolLogger = poolLogger
|
||||
poolLogger[metadataKey: RedisLogging.MetadataKeys.connectionPoolID] = "\(self.id)"
|
||||
self.poolSystemContext = poolLogger
|
||||
|
||||
self.connectionTCPClient = connectionTCPClient
|
||||
|
||||
|
||||
var taggedConnectionLogger = config.factoryConfiguration.connectionDefaultLogger
|
||||
taggedConnectionLogger[metadataKey: RedisLogging.MetadataKeys.connectionPoolID] = "\(self.id)"
|
||||
config.factoryConfiguration.connectionDefaultLogger = taggedConnectionLogger
|
||||
|
||||
var taggedPoolLogger = config.poolDefaultLogger
|
||||
taggedPoolLogger[metadataKey: RedisLogging.MetadataKeys.connectionPoolID] = "\(self.id)"
|
||||
config.poolDefaultLogger = taggedPoolLogger
|
||||
|
||||
self.configuration = config
|
||||
|
||||
self.pool = ConnectionPool(
|
||||
maximumConnectionCount: maximumConnectionCount.size,
|
||||
minimumConnectionCount: minimumConnectionCount,
|
||||
leaky: maximumConnectionCount.leaky,
|
||||
loop: loop,
|
||||
systemContext: poolLogger,
|
||||
connectionBackoffFactor: connectionBackoffFactor,
|
||||
initialConnectionBackoffDelay: initialConnectionBackoffDelay,
|
||||
maximumConnectionCount: config.maximumConnectionCount.size,
|
||||
minimumConnectionCount: config.minimumConnectionCount,
|
||||
leaky: config.maximumConnectionCount.leaky,
|
||||
loop: boundEventLoop,
|
||||
systemContext: config.poolDefaultLogger,
|
||||
connectionBackoffFactor: config.connectionRetryConfiguration.backoff.factor,
|
||||
initialConnectionBackoffDelay: config.connectionRetryConfiguration.backoff.initialDelay,
|
||||
connectionFactory: self.connectionFactory(_:)
|
||||
)
|
||||
}
|
||||
@@ -190,7 +154,7 @@ extension RedisConnectionPool {
|
||||
}
|
||||
|
||||
/// Updates the list of valid connection addresses.
|
||||
///
|
||||
/// - Warning: This will replace any previously set list of addresses.
|
||||
/// - Note: This does not invalidate existing connections: as long as those connections continue to stay up, they will be kept by
|
||||
/// this client.
|
||||
///
|
||||
@@ -214,26 +178,42 @@ extension RedisConnectionPool {
|
||||
// Validate the loop invariants.
|
||||
self.loop.preconditionInEventLoop()
|
||||
targetLoop.preconditionInEventLoop()
|
||||
|
||||
let factoryConfig = self.configuration.factoryConfiguration
|
||||
|
||||
guard let nextTarget = self.serverConnectionAddresses.nextTarget() else {
|
||||
// No valid connection target, we'll fail.
|
||||
return targetLoop.makeFailedFuture(RedisConnectionPoolError.noAvailableConnectionTargets)
|
||||
}
|
||||
|
||||
let connectionConfig: RedisConnection.Configuration
|
||||
do {
|
||||
connectionConfig = try .init(
|
||||
address: nextTarget,
|
||||
password: factoryConfig.connectionPassword,
|
||||
initialDatabase: factoryConfig.connectionInitialDatabase,
|
||||
defaultLogger: factoryConfig.connectionDefaultLogger
|
||||
)
|
||||
} catch {
|
||||
// config validation failed, return the error
|
||||
return targetLoop.makeFailedFuture(error)
|
||||
}
|
||||
|
||||
let connectFuture = RedisConnection.connect(
|
||||
to: nextTarget,
|
||||
on: targetLoop,
|
||||
password: self.connectionPassword,
|
||||
logger: self.connectionSystemContext,
|
||||
tcpClient: self.connectionTCPClient
|
||||
)
|
||||
// disallow subscriptions on all connections by default so that we can enforce our management of PubSub state
|
||||
connectFuture.whenSuccess { $0.allowSubscriptions = false }
|
||||
return connectFuture
|
||||
return RedisConnection
|
||||
.make(
|
||||
configuration: connectionConfig,
|
||||
boundEventLoop: targetLoop,
|
||||
configuredTCPClient: factoryConfig.tcpClient
|
||||
)
|
||||
.map { connection in
|
||||
// disallow subscriptions on all connections by default to enforce our management of PubSub state
|
||||
connection.allowSubscriptions = false
|
||||
return connection
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareLoggerForUse(_ logger: Logger?) -> Logger {
|
||||
guard var logger = logger else { return self.poolSystemContext }
|
||||
guard var logger = logger else { return self.configuration.poolDefaultLogger }
|
||||
logger[metadataKey: RedisLogging.MetadataKeys.connectionPoolID] = "\(self.id)"
|
||||
return logger
|
||||
}
|
||||
@@ -441,7 +421,11 @@ extension RedisConnectionPool: RedisClientWithUserContext {
|
||||
let logger = self.prepareLoggerForUse(context)
|
||||
|
||||
guard let connection = preferredConnection else {
|
||||
return pool.leaseConnection(deadline: .now() + self.connectionRetryTimeout, logger: logger)
|
||||
return pool
|
||||
.leaseConnection(
|
||||
deadline: .now() + self.configuration.connectionRetryConfiguration.timeout,
|
||||
logger: logger
|
||||
)
|
||||
.flatMap { operation($0, pool.returnConnection(_:logger:), logger) }
|
||||
}
|
||||
|
||||
@@ -456,57 +440,47 @@ extension RedisConnectionPool {
|
||||
private var addresses: [SocketAddress]
|
||||
|
||||
private var index: Array<SocketAddress>.Index
|
||||
|
||||
init(initialAddresses: [SocketAddress]) {
|
||||
|
||||
internal init(initialAddresses: [SocketAddress]) {
|
||||
self.addresses = initialAddresses
|
||||
self.index = self.addresses.startIndex
|
||||
}
|
||||
|
||||
mutating func nextTarget() -> SocketAddress? {
|
||||
// Early exit on 0, makes life easier.
|
||||
guard self.addresses.count > 0 else {
|
||||
|
||||
internal mutating func nextTarget() -> SocketAddress? {
|
||||
// early exit on 0, makes life easier
|
||||
guard !self.addresses.isEmpty else {
|
||||
self.index = self.addresses.startIndex
|
||||
return nil
|
||||
}
|
||||
|
||||
// It's an invariant of this function that the index is always valid for subscripting the collection.
|
||||
|
||||
let nextTarget = self.addresses[self.index]
|
||||
|
||||
// it's an invariant of this function that the index is always valid for subscripting the collection
|
||||
self.addresses.formIndex(after: &self.index)
|
||||
if self.index == self.addresses.endIndex {
|
||||
self.index = self.addresses.startIndex
|
||||
}
|
||||
|
||||
return nextTarget
|
||||
}
|
||||
|
||||
mutating func update(_ newAddresses: [SocketAddress]) {
|
||||
|
||||
internal mutating func update(_ newAddresses: [SocketAddress]) {
|
||||
self.addresses = newAddresses
|
||||
self.index = self.addresses.startIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `RedisConnectionPoolSize` controls how the maximum number of connections in a pool are interpreted.
|
||||
public enum RedisConnectionPoolSize {
|
||||
/// The pool will allow no more than this number of connections to be "active" (that is, connecting, in-use,
|
||||
/// or pooled) at any one time. This will force possible future users of new connections to wait until a currently
|
||||
/// active connection becomes available by being returned to the pool, but provides a hard upper limit on concurrency.
|
||||
case maximumActiveConnections(Int)
|
||||
|
||||
/// The pool will only store up to this number of connections that are not currently in-use. However, if the pool is
|
||||
/// asked for more connections at one time than this number, it will create new connections to serve those waiting for
|
||||
/// connections. These "extra" connections will not be preserved: while they will be used to satisfy those waiting for new
|
||||
/// connections if needed, they will not be preserved in the pool if load drops low enough. This does not provide a hard
|
||||
/// upper bound on concurrency, but does provide an upper bound on low-level load.
|
||||
case maximumPreservedConnections(Int)
|
||||
|
||||
internal var size: Int {
|
||||
// MARK: RedisConnectionPoolSize helpers
|
||||
extension RedisConnectionPoolSize {
|
||||
fileprivate var size: Int {
|
||||
switch self {
|
||||
case .maximumActiveConnections(let size), .maximumPreservedConnections(let size):
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
internal var leaky: Bool {
|
||||
fileprivate var leaky: Bool {
|
||||
switch self {
|
||||
case .maximumActiveConnections:
|
||||
return false
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the RediStack open source project
|
||||
//
|
||||
// Copyright (c) 2020 RediStack project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of RediStack project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Logging
|
||||
import NIO
|
||||
|
||||
extension RedisConnection {
|
||||
/// The documented default port that Redis connects through.
|
||||
///
|
||||
/// See [https://redis.io/topics/quickstart](https://redis.io/topics/quickstart)
|
||||
@available(*, deprecated, message: "Use RedisConnection.Configuration.defaultPort")
|
||||
public static var defaultPort: Int { Configuration.defaultPort }
|
||||
|
||||
/// Creates a new connection to a Redis instance.
|
||||
///
|
||||
/// If you would like to specialize the `NIO.ClientBootstrap` that the connection communicates on, override the default by passing it in as `tcpClient`.
|
||||
///
|
||||
/// let eventLoopGroup: EventLoopGroup = ...
|
||||
/// var customTCPClient = ClientBootstrap.makeRedisTCPClient(group: eventLoopGroup)
|
||||
/// customTCPClient.channelInitializer { channel in
|
||||
/// // channel customizations
|
||||
/// }
|
||||
/// let connection = RedisConnection.connect(
|
||||
/// to: ...,
|
||||
/// on: eventLoopGroup.next(),
|
||||
/// password: ...,
|
||||
/// tcpClient: customTCPClient
|
||||
/// ).wait()
|
||||
///
|
||||
/// It is recommended that you be familiar with `ClientBootstrap.makeRedisTCPClient(group:)` and `NIO.ClientBootstrap` in general before doing so.
|
||||
///
|
||||
/// Note: Use of `wait()` in the example is for simplicity. Never call `wait()` on an event loop.
|
||||
///
|
||||
/// - Important: Call `close()` on the connection before letting the instance deinit to properly cleanup resources.
|
||||
/// - Note: If a `password` is provided, the connection will send an "AUTH" command to Redis as soon as it has been opened.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - socket: The `NIO.SocketAddress` information of the Redis instance to connect to.
|
||||
/// - eventLoop: The `NIO.EventLoop` that this connection will execute all tasks on.
|
||||
/// - password: The optional password to use for authorizing the connection with Redis.
|
||||
/// - logger: The `Logging.Logger` instance to use for all client logging purposes. If one is not provided, one will be created.
|
||||
/// A `Foundation.UUID` will be attached to the metadata to uniquely identify this connection instance's logs.
|
||||
/// - tcpClient: If you have chosen to configure a `NIO.ClientBootstrap` yourself, this will be used instead of the `makeRedisTCPClient` instance.
|
||||
/// - Returns: A `NIO.EventLoopFuture` that resolves with the new connection after it has been opened, and if a `password` is provided, authenticated.
|
||||
@available(*, deprecated, message: "Use make(configuration:boundEventLoop:configuredTCPClient:) instead")
|
||||
public static func connect(
|
||||
to socket: SocketAddress,
|
||||
on eventLoop: EventLoop,
|
||||
password: String? = nil,
|
||||
logger: Logger = .redisBaseConnectionLogger,
|
||||
tcpClient: ClientBootstrap? = nil
|
||||
) -> EventLoopFuture<RedisConnection> {
|
||||
let config: Configuration
|
||||
do {
|
||||
config = try .init(
|
||||
address: socket,
|
||||
password: password,
|
||||
defaultLogger: logger
|
||||
)
|
||||
} catch {
|
||||
return eventLoop.makeFailedFuture(error)
|
||||
}
|
||||
|
||||
return self.make(configuration: config, boundEventLoop: eventLoop, configuredTCPClient: tcpClient)
|
||||
}
|
||||
}
|
||||
|
||||
extension RedisConnectionPool {
|
||||
/// Create a new `RedisConnectionPool`.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - serverConnectionAddresses: The set of Redis servers to which this pool is initially willing to connect.
|
||||
/// This set can be updated over time.
|
||||
/// - loop: The event loop to which this pooled client is tied.
|
||||
/// - maximumConnectionCount: The maximum number of connections to for this pool, either to be preserved or as a hard limit.
|
||||
/// - minimumConnectionCount: The minimum number of connections to preserve in the pool. If the pool is mostly idle
|
||||
/// and the Redis servers close these idle connections, the `RedisConnectionPool` will initiate new outbound
|
||||
/// connections proactively to avoid the number of available connections dropping below this number. Defaults to `1`.
|
||||
/// - connectionPassword: The password to use to connect to the Redis servers in this pool.
|
||||
/// - connectionLogger: The `Logger` to pass to each connection in the pool.
|
||||
/// - connectionTCPClient: The base `ClientBootstrap` to use to create pool connections, if a custom one is in use.
|
||||
/// - poolLogger: The `Logger` used by the connection pool itself.
|
||||
/// - connectionBackoffFactor: Used when connection attempts fail to control the exponential backoff. This is a multiplicative
|
||||
/// factor, each connection attempt will be delayed by this amount times the previous delay.
|
||||
/// - initialConnectionBackoffDelay: If a TCP connection attempt fails, this is the first backoff value on the reconnection attempt.
|
||||
/// Subsequent backoffs are computed by compounding this value by `connectionBackoffFactor`.
|
||||
/// - connectionRetryTimeout: The max time to wait for a connection to be available before failing a particular command or connection operation.
|
||||
/// The default is 60 seconds.
|
||||
@available(*, deprecated, message: "Use .init(configuration:boundEventLoop:) instead.")
|
||||
public convenience init(
|
||||
serverConnectionAddresses: [SocketAddress],
|
||||
loop: EventLoop,
|
||||
maximumConnectionCount: RedisConnectionPoolSize,
|
||||
minimumConnectionCount: Int = 1,
|
||||
connectionPassword: String? = nil, // config
|
||||
connectionLogger: Logger = .redisBaseConnectionLogger, // config
|
||||
connectionTCPClient: ClientBootstrap? = nil,
|
||||
poolLogger: Logger = .redisBaseConnectionPoolLogger,
|
||||
connectionBackoffFactor: Float32 = 2,
|
||||
initialConnectionBackoffDelay: TimeAmount = .milliseconds(100),
|
||||
connectionRetryTimeout: TimeAmount? = .seconds(60)
|
||||
) {
|
||||
self.init(
|
||||
configuration: Configuration(
|
||||
initialServerConnectionAddresses: serverConnectionAddresses,
|
||||
maximumConnectionCount: maximumConnectionCount,
|
||||
connectionFactoryConfiguration: ConnectionFactoryConfiguration(
|
||||
connectionPassword: connectionPassword,
|
||||
connectionDefaultLogger: connectionLogger,
|
||||
tcpClient: connectionTCPClient
|
||||
),
|
||||
minimumConnectionCount: minimumConnectionCount,
|
||||
connectionBackoffFactor: connectionBackoffFactor,
|
||||
initialConnectionBackoffDelay: initialConnectionBackoffDelay,
|
||||
connectionRetryTimeout: connectionRetryTimeout,
|
||||
poolDefaultLogger: poolLogger
|
||||
),
|
||||
boundEventLoop: loop
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// This source file is part of the RediStack open source project
|
||||
//
|
||||
// Copyright (c) 2019 RediStack project authors
|
||||
// Copyright (c) 2019-2020 RediStack project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
@@ -16,32 +16,15 @@ import Foundation
|
||||
import NIO
|
||||
import RediStack
|
||||
|
||||
extension RedisConnection {
|
||||
extension RedisConnection.Configuration {
|
||||
/// A default hostname of `localhost` to try and connect to Redis at.
|
||||
public static let defaultHostname = "localhost"
|
||||
|
||||
/// Creates a connection intended for tests using `REDIS_URL` and `REDIS_PW` environment variables if available.
|
||||
///
|
||||
/// The default URL is `127.0.0.1` while the default port is `RedisConnection.defaultPort`.
|
||||
///
|
||||
/// If `REDIS_PW` is not defined, no authentication will happen on the connection.
|
||||
/// - Parameters:
|
||||
/// - eventLoop: The event loop that the connection should execute on.
|
||||
/// - port: The port to connect on.
|
||||
/// - Returns: A `NIO.EventLoopFuture` that resolves with the new connection.
|
||||
public static func connect(
|
||||
on eventLoop: EventLoop,
|
||||
host: String = RedisConnection.defaultHostname,
|
||||
port: Int = RedisConnection.defaultPort,
|
||||
|
||||
public init(
|
||||
host: String = RedisConnection.Configuration.defaultHostname,
|
||||
port: Int = RedisConnection.Configuration.defaultPort,
|
||||
password: String? = nil
|
||||
) -> EventLoopFuture<RedisConnection> {
|
||||
let address: SocketAddress
|
||||
do {
|
||||
address = try SocketAddress.makeAddressResolvingHost(host, port: port)
|
||||
} catch {
|
||||
return eventLoop.makeFailedFuture(error)
|
||||
}
|
||||
|
||||
return RedisConnection.connect(to: address, on: eventLoop, password: password)
|
||||
) throws {
|
||||
try self.init(hostname: host, port: port, password: password)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ open class RedisConnectionPoolIntegrationTestCase: XCTestCase {
|
||||
/// The default value is `RedisConnection.defaultHostname`
|
||||
///
|
||||
/// This is especially useful to override if you build on Linux & macOS where Redis might be installed locally vs. through Docker.
|
||||
open var redisHostname: String { return RedisConnection.defaultHostname }
|
||||
open var redisHostname: String { RedisConnection.Configuration.defaultHostname }
|
||||
|
||||
/// The port to connect over to Redis, defaulting to `RedisConnection.defaultPort`.
|
||||
open var redisPort: Int { return RedisConnection.defaultPort }
|
||||
open var redisPort: Int { RedisConnection.Configuration.defaultPort }
|
||||
|
||||
/// The password to use to connect to Redis. Default is `nil` - no password authentication.
|
||||
open var redisPassword: String? { return nil }
|
||||
@@ -80,12 +80,14 @@ open class RedisConnectionPoolIntegrationTestCase: XCTestCase {
|
||||
) throws -> RedisConnectionPool {
|
||||
let address = try SocketAddress.makeAddressResolvingHost(self.redisHostname, port: self.redisPort)
|
||||
let pool = RedisConnectionPool(
|
||||
serverConnectionAddresses: [address],
|
||||
loop: self.eventLoopGroup.next(),
|
||||
maximumConnectionCount: .maximumActiveConnections(4),
|
||||
minimumConnectionCount: minimumConnectionCount,
|
||||
connectionPassword: self.redisPassword,
|
||||
connectionRetryTimeout: connectionRetryTimeout
|
||||
configuration: .init(
|
||||
initialServerConnectionAddresses: [address],
|
||||
maximumConnectionCount: .maximumActiveConnections(4),
|
||||
connectionFactoryConfiguration: .init(connectionPassword: self.redisPassword),
|
||||
minimumConnectionCount: minimumConnectionCount,
|
||||
connectionRetryTimeout: connectionRetryTimeout
|
||||
),
|
||||
boundEventLoop: self.eventLoopGroup.next()
|
||||
)
|
||||
pool.activate()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// This source file is part of the RediStack open source project
|
||||
//
|
||||
// Copyright (c) 2019 RediStack project authors
|
||||
// Copyright (c) 2019-2020 RediStack project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
@@ -18,17 +18,17 @@ import XCTest
|
||||
|
||||
/// A helper `XCTestCase` subclass that does the standard work of creating a connection to use in test cases.
|
||||
///
|
||||
/// See `RedisConnection.connect(to:port:)` to understand how connections are made.
|
||||
/// See `RedisConnection.make(configuration:boundEventLoop:)` to understand how connections are made.
|
||||
open class RedisIntegrationTestCase: XCTestCase {
|
||||
/// An overridable value of the Redis instance's hostname to connect to for the test suite(s).
|
||||
///
|
||||
/// The default value is `RedisConnection.defaultHostname`
|
||||
///
|
||||
/// This is especially useful to override if you build on Linux & macOS where Redis might be installed locally vs. through Docker.
|
||||
open var redisHostname: String { return RedisConnection.defaultHostname }
|
||||
open var redisHostname: String { RedisConnection.Configuration.defaultHostname }
|
||||
|
||||
/// The port to connect over to Redis, defaulting to `RedisConnection.defaultPort`.
|
||||
open var redisPort: Int { return RedisConnection.defaultPort }
|
||||
open var redisPort: Int { RedisConnection.Configuration.defaultPort }
|
||||
|
||||
/// The password to use to connect to Redis. Default is `nil` - no password authentication.
|
||||
open var redisPassword: String? { return nil }
|
||||
@@ -77,14 +77,16 @@ open class RedisIntegrationTestCase: XCTestCase {
|
||||
|
||||
/// Creates a new connection for use in tests.
|
||||
///
|
||||
/// See `RedisConnection.connect(to:port:)`
|
||||
/// See `RedisConnection.make(configuration:boundEventLoop:)`
|
||||
/// - Returns: The new `RediStack.RedisConnection`.
|
||||
public func makeNewConnection() throws -> RedisConnection {
|
||||
return try RedisConnection.connect(
|
||||
on: eventLoopGroup.next(),
|
||||
host: self.redisHostname,
|
||||
port: self.redisPort,
|
||||
password: self.redisPassword
|
||||
return try RedisConnection.make(
|
||||
configuration: .init(
|
||||
host: self.redisHostname,
|
||||
port: self.redisPort,
|
||||
password: self.redisPassword
|
||||
),
|
||||
boundEventLoop: eventLoopGroup.next()
|
||||
).wait()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the RediStack open source project
|
||||
//
|
||||
// Copyright (c) 2020 RediStack project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of RediStack project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import NIO
|
||||
import RediStack
|
||||
|
||||
extension RedisConnection {
|
||||
/// A default hostname of `localhost` to try and connect to Redis at.
|
||||
@available(*, deprecated, message: "Use RedisConnection.Configuration.defaultHostname")
|
||||
public static let defaultHostname = "localhost"
|
||||
|
||||
/// Creates a connection intended for tests using `REDIS_URL` and `REDIS_PW` environment variables if available.
|
||||
///
|
||||
/// The default URL is `127.0.0.1` while the default port is `RedisConnection.defaultPort`.
|
||||
///
|
||||
/// If `REDIS_PW` is not defined, no authentication will happen on the connection.
|
||||
/// - Parameters:
|
||||
/// - eventLoop: The event loop that the connection should execute on.
|
||||
/// - port: The port to connect on.
|
||||
/// - Returns: A `NIO.EventLoopFuture` that resolves with the new connection.
|
||||
@available(*, deprecated, message: "Use RedisConnection.make(configuration:boundEventLoop:) method")
|
||||
public static func connect(
|
||||
on eventLoop: EventLoop,
|
||||
host: String = RedisConnection.defaultHostname,
|
||||
port: Int = RedisConnection.Configuration.defaultPort,
|
||||
password: String? = nil
|
||||
) -> EventLoopFuture<RedisConnection> {
|
||||
let address: SocketAddress
|
||||
do {
|
||||
address = try SocketAddress.makeAddressResolvingHost(host, port: port)
|
||||
} catch {
|
||||
return eventLoop.makeFailedFuture(error)
|
||||
}
|
||||
|
||||
return RedisConnection.connect(to: address, on: eventLoop, password: password)
|
||||
}
|
||||
}
|
||||
@@ -47,10 +47,12 @@ final class RedisLoggingTests: RediStackIntegrationTestCase {
|
||||
let logger = Logger(label: #function, factory: { _ in return handler })
|
||||
|
||||
let pool = RedisConnectionPool(
|
||||
serverConnectionAddresses: [try .makeAddressResolvingHost(self.redisHostname, port: self.redisPort)],
|
||||
loop: self.connection.eventLoop,
|
||||
maximumConnectionCount: .maximumActiveConnections(1),
|
||||
connectionPassword: self.redisPassword
|
||||
configuration: .init(
|
||||
initialServerConnectionAddresses: [try .makeAddressResolvingHost(self.redisHostname, port: self.redisPort)],
|
||||
maximumConnectionCount: .maximumActiveConnections(1),
|
||||
connectionFactoryConfiguration: .init(connectionPassword: self.redisPassword)
|
||||
),
|
||||
boundEventLoop: self.connection.eventLoop
|
||||
)
|
||||
defer { pool.close() }
|
||||
pool.activate()
|
||||
|
||||
@@ -29,9 +29,9 @@ final class RedisCommandHandlerTests: XCTestCase {
|
||||
.wait()
|
||||
defer { try? server.close().wait() }
|
||||
|
||||
let connection = try RedisConnection.connect(
|
||||
to: socketAddress,
|
||||
on: group.next()
|
||||
let connection = try RedisConnection.make(
|
||||
configuration: .init(hostname: "localhost", port: 8080),
|
||||
boundEventLoop: group.next()
|
||||
).wait()
|
||||
defer { try? connection.close().wait() }
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the RediStack open source project
|
||||
//
|
||||
// Copyright (c) 2020 RediStack project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of RediStack project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import NIO
|
||||
import RediStack
|
||||
import XCTest
|
||||
|
||||
final class ConfigurationTests: XCTestCase {
|
||||
func test_connectionConfiguration_stringURLValidation() throws {
|
||||
XCTAssertNoThrow(try RedisConnection.Configuration(url: "redis://localhost:6379"))
|
||||
XCTAssertNoThrow(try RedisConnection.Configuration(url: "redis://localhost:6379/0"))
|
||||
XCTAssertNoThrow(try RedisConnection.Configuration(url: "redis://:password@localhost"))
|
||||
XCTAssertThrowsError(try RedisConnection.Configuration(url: "localhost:6379"))
|
||||
XCTAssertThrowsError(try RedisConnection.Configuration(url: "redis://:6379"))
|
||||
XCTAssertThrowsError(try RedisConnection.Configuration(url: "redis://localhost/-1"))
|
||||
}
|
||||
|
||||
func test_connectionConfiguration_urlComponents() throws {
|
||||
let configuration = try RedisConnection.Configuration(url: "redis://:password@localhost:6666/1")
|
||||
XCTAssertEqual(configuration.hostname, "localhost")
|
||||
XCTAssertEqual(configuration.password, "password")
|
||||
XCTAssertEqual(configuration.port, 6666)
|
||||
XCTAssertEqual(configuration.initialDatabase, 1)
|
||||
}
|
||||
|
||||
func test_connectionConfiguration_databaseIndex() throws {
|
||||
let address = try SocketAddress.makeAddressResolvingHost("localhost", port: RedisConnection.Configuration.defaultPort)
|
||||
XCTAssertThrowsError(try RedisConnection.Configuration(address: address, initialDatabase: -1)) {
|
||||
XCTAssertEqual($0 as? RedisConnection.Configuration.ValidationError, .outOfBoundsDatabaseID)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user