diff --git a/Benchmarks/Benchmarks/ConnectionPoolBenchmarks/ConnectionPoolBenchmarks.swift b/Benchmarks/Benchmarks/ConnectionPoolBenchmarks/ConnectionPoolBenchmarks.swift index 9cc535d4..c988c4b3 100644 --- a/Benchmarks/Benchmarks/ConnectionPoolBenchmarks/ConnectionPoolBenchmarks.swift +++ b/Benchmarks/Benchmarks/ConnectionPoolBenchmarks/ConnectionPoolBenchmarks.swift @@ -1,23 +1,27 @@ import _ConnectionPoolModule import _ConnectionPoolTestUtils import Benchmark +import NIOCore +import NIOPosix let benchmarks: @Sendable () -> Void = { - Benchmark("Lease/Release 1k requests: 50 parallel", configuration: .init(scalingFactor: .kilo)) { benchmark in + Benchmark("Pool: Lease/Release 1k requests: 50 parallel", configuration: .init(scalingFactor: .kilo)) { benchmark in let clock = MockClock() - let factory = MockConnectionFactory(autoMaxStreams: 1) + let factory = MockConnectionFactory(autoMaxStreams: 1) var configuration = ConnectionPoolConfiguration() configuration.maximumConnectionSoftLimit = 50 configuration.maximumConnectionHardLimit = 50 let pool = ConnectionPool( configuration: configuration, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), keepAliveBehavior: MockPingPongBehavior(keepAliveFrequency: nil, connectionType: MockConnection.self), - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } await withTaskGroup { taskGroup in @@ -54,21 +58,23 @@ let benchmarks: @Sendable () -> Void = { } } - Benchmark("Lease/Release 1k requests: sequential", configuration: .init(scalingFactor: .kilo)) { benchmark in + Benchmark("Pool: Lease/Release 1k requests: sequential", configuration: .init(scalingFactor: .kilo)) { benchmark in let clock = MockClock() - let factory = MockConnectionFactory(autoMaxStreams: 1) + let factory = MockConnectionFactory(autoMaxStreams: 1) var configuration = ConnectionPoolConfiguration() configuration.maximumConnectionSoftLimit = 50 configuration.maximumConnectionHardLimit = 50 let pool = ConnectionPool( configuration: configuration, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), keepAliveBehavior: MockPingPongBehavior(keepAliveFrequency: nil, connectionType: MockConnection.self), - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } await withTaskGroup { taskGroup in @@ -96,4 +102,123 @@ let benchmarks: @Sendable () -> Void = { taskGroup.cancelAll() } } + + Benchmark("PoolManager/TaskExecutor: Lease/Release 1k requests: 50 parallel – 10 MockExecutors", configuration: .init(scalingFactor: .kilo)) { benchmark in + let clock = MockClock() + let factory = MockConnectionFactory(autoMaxStreams: 1) + var configuration = ConnectionPoolManagerConfiguration() + let executorCount = 10 + let executors = (0...self, + keepAliveBehavior: MockPingPongBehavior(keepAliveFrequency: nil, connectionType: MockConnection.self), + executors: executors, + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + clock: clock + ) { + try await factory.makeConnection(id: $0, configuration: $1, for: $2) + } + + await withTaskGroup { taskGroup in + taskGroup.addTask { + await pool.run() + } + + let sequential = benchmark.scaledIterations.upperBound / concurrency + + benchmark.startMeasurement() + + for parallel in 0..(autoMaxStreams: 1) + var configuration = ConnectionPoolManagerConfiguration() + try await NIOTaskExecutor.withExecutors(eventLoops) { executors in + let concurrency = 50 + + configuration.maximumConnectionPerExecutorSoftLimit = concurrency / executors.count + configuration.maximumConnectionPerExecutorHardLimit = concurrency / executors.count + + let pool = ConnectionPoolManager( + configuration: configuration, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), + idGenerator: ConnectionIDGenerator(), + requestType: ConnectionRequest.self, + keepAliveBehavior: MockPingPongBehavior(keepAliveFrequency: nil, connectionType: MockConnection.self), + executors: executors, + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + clock: clock + ) { + try await factory.makeConnection(id: $0, configuration: $1, for: $2) + } + + await withTaskGroup { taskGroup in + taskGroup.addTask { + await pool.run() + } + + let sequential = benchmark.scaledIterations.upperBound / executors.count + + benchmark.startMeasurement() + + for executor in executors { + taskGroup.addTask(executorPreference: executor) { + for _ in 0..() + + let eventLoop: any EventLoop + + private init(eventLoop: any EventLoop) { + self.eventLoop = eventLoop + } + + static func withExecutors(_ eventLoops: MultiThreadedEventLoopGroup, _ body: ([NIOTaskExecutor]) async throws -> ()) async throws { + var executors = [NIOTaskExecutor]() + for eventLoop in eventLoops.makeIterator() { + let executor = NIOTaskExecutor(eventLoop: eventLoop) + try await eventLoop.submit { + NIOTaskExecutor.threadSpecificEventLoop.currentValue = executor + }.get() + executors.append(executor) + } + do { + try await body(executors) + } catch { + + } + for eventLoop in eventLoops.makeIterator() { + try await eventLoop.submit { + NIOTaskExecutor.threadSpecificEventLoop.currentValue = nil + }.get() + } + } +} + +@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *) +extension NIOTaskExecutor: TaskExecutor { + + func enqueue(_ job: consuming ExecutorJob) { + // By default we are just going to use execute to run the job + // this is quite heavy since it allocates the closure for + // every single job. + let unownedJob = UnownedJob(job) + self.eventLoop.execute { + unownedJob.runSynchronously(on: self.asUnownedTaskExecutor()) + } + } + + func asUnownedTaskExecutor() -> UnownedTaskExecutor { + UnownedTaskExecutor(ordinary: self) + } +} + +@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *) +extension NIOTaskExecutor: ConnectionPoolExecutor { + typealias ID = ObjectIdentifier + + var id: ObjectIdentifier { + ObjectIdentifier(self) + } + + static func getExecutorID() -> ObjectIdentifier? { + self.threadSpecificEventLoop.currentValue?.id + } +} diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index 11407176..68166b3c 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -10,6 +10,8 @@ let package = Package( dependencies: [ .package(path: "../"), .package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.29.0"), + .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.14.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.82.0"), ], targets: [ .executableTarget( @@ -18,11 +20,22 @@ let package = Package( .product(name: "_ConnectionPoolModule", package: "postgres-nio"), .product(name: "_ConnectionPoolTestUtils", package: "postgres-nio"), .product(name: "Benchmark", package: "package-benchmark"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), ], path: "Benchmarks/ConnectionPoolBenchmarks", plugins: [ .plugin(name: "BenchmarkPlugin", package: "package-benchmark") ] ), + .executableTarget( + name: "PostgresPerf", + dependencies: [ + .product(name: "PostgresNIO", package: "postgres-nio"), + .product(name: "PostgresKit", package: "postgres-kit"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ], + ) ] ) diff --git a/Benchmarks/Sources/PostgresPerf/PostgresPerf.swift b/Benchmarks/Sources/PostgresPerf/PostgresPerf.swift new file mode 100644 index 00000000..19b9353c --- /dev/null +++ b/Benchmarks/Sources/PostgresPerf/PostgresPerf.swift @@ -0,0 +1,131 @@ +// +// PostgresPerf.swift +// benchmarks +// +// Created by Fabian Fett on 12.05.25. +// + +import Synchronization +import PostgresNIO +@preconcurrency import PostgresKit +@preconcurrency import AsyncKit + +@main +@available(macOS 15.0, *) +enum PostgresPerf { + + static let maxConnections: Int = 50 + static let tasks: Int = 400 + static let iterationsPerTask: Int = 1000 + static let logger = Logger(label: "TestLogger") + static let clock = ContinuousClock() + + static let eventLoopCount = { + NIOSingletons.posixEventLoopGroup.makeIterator().reduce(0, { (res, _) in res + 1 }) + }() + + static func main() async throws { +// if CommandLine.arguments.first == "kit" { + try await Self.runPostgresKit() +// } else { + try await self.runPostgresNIO() +// } + } + + static func runPostgresKit() async throws { + let configuration = SQLPostgresConfiguration( + hostname: "localhost", port: 5432, + username: "test_username", + password: "test_password", + database: "test_database", + tls: .disable + ) + + let pools = EventLoopGroupConnectionPool( + source: PostgresConnectionSource(sqlConfiguration: configuration), + maxConnectionsPerEventLoop: Self.maxConnections / Self.eventLoopCount, + on: NIOSingletons.posixEventLoopGroup + ) + + let start = self.clock.now + await withThrowingTaskGroup(of: Void.self) { taskGroup in + for _ in 0.. String? { + getenv(name).flatMap { String(cString: $0) } +} diff --git a/Sources/ConnectionPoolModule/ConnectionIDGenerator.swift b/Sources/ConnectionPoolModule/ConnectionIDGenerator.swift index b428d805..ea9ff216 100644 --- a/Sources/ConnectionPoolModule/ConnectionIDGenerator.swift +++ b/Sources/ConnectionPoolModule/ConnectionIDGenerator.swift @@ -1,7 +1,7 @@ import Atomics public struct ConnectionIDGenerator: ConnectionIDGeneratorProtocol { - static let globalGenerator = ConnectionIDGenerator() + public static let globalGenerator = ConnectionIDGenerator() private let atomic: ManagedAtomic diff --git a/Sources/ConnectionPoolModule/ConnectionPool.swift b/Sources/ConnectionPoolModule/ConnectionPool.swift index 40d52a5a..eabbdc30 100644 --- a/Sources/ConnectionPoolModule/ConnectionPool.swift +++ b/Sources/ConnectionPoolModule/ConnectionPool.swift @@ -134,9 +134,11 @@ public final class ConnectionPool< Connection: PooledConnection, ConnectionID: Hashable & Sendable, ConnectionIDGenerator: ConnectionIDGeneratorProtocol, + ConnectionConfiguration: Equatable & Sendable, Request: ConnectionRequestProtocol, RequestID: Hashable & Sendable, KeepAliveBehavior: ConnectionKeepAliveBehavior, + Executor: ConnectionPoolExecutor, ObservabilityDelegate: ConnectionPoolObservabilityDelegate, Clock: _Concurrency.Clock >: Sendable where @@ -148,7 +150,7 @@ public final class ConnectionPool< ObservabilityDelegate.ConnectionID == ConnectionID, Clock.Duration == Duration { - public typealias ConnectionFactory = @Sendable (ConnectionID, ConnectionPool) async throws -> ConnectionAndMetadata + public typealias ConnectionFactory = @Sendable (ConnectionID, ConnectionConfiguration, ConnectionPool) async throws -> ConnectionAndMetadata @usableFromInline typealias StateMachine = PoolStateMachine> @@ -156,6 +158,11 @@ public final class ConnectionPool< @usableFromInline let factory: ConnectionFactory + public let executor: Executor + + @usableFromInline + let connectionConfiguration: ConnectionConfiguration + @usableFromInline let keepAliveBehavior: KeepAliveBehavior @@ -188,18 +195,22 @@ public final class ConnectionPool< public init( configuration: ConnectionPoolConfiguration, + connectionConfiguration: ConnectionConfiguration, idGenerator: ConnectionIDGenerator, requestType: Request.Type, keepAliveBehavior: KeepAliveBehavior, + executor: Executor, observabilityDelegate: ObservabilityDelegate, clock: Clock, connectionFactory: @escaping ConnectionFactory ) { + self.executor = executor self.clock = clock self.factory = connectionFactory self.keepAliveBehavior = keepAliveBehavior self.observabilityDelegate = observabilityDelegate self.configuration = configuration + self.connectionConfiguration = connectionConfiguration var stateMachine = StateMachine( configuration: .init(configuration, keepAliveBehavior: keepAliveBehavior), generator: idGenerator, @@ -253,10 +264,19 @@ public final class ConnectionPool< } } - public func cancelLeaseConnection(_ requestID: RequestID) { + @discardableResult + public func cancelLeaseConnection(_ requestID: RequestID) -> Bool { + var found = false self.modifyStateAndRunActions { state in - state.stateMachine.cancelRequest(id: requestID) + let action = state.stateMachine.cancelRequest(id: requestID) + if case .failRequest = action.request { + found = true + } else { + found = false + } + return action } + return found } /// Mark a connection as going away. Connection implementors have to call this method if the connection @@ -271,6 +291,13 @@ public final class ConnectionPool< } } + @inlinable + public func updateConfiguration(_ configuration: ConnectionConfiguration, forceReconnection: Bool) { + // TODO: Implement connection will close correctly + // If the forceReconnection flag is set, we should gracefully close the connection once they + // are returned the next time. + } + @inlinable public func run() async { await withTaskCancellationHandler { @@ -425,11 +452,11 @@ public final class ConnectionPool< @inlinable /*private*/ func makeConnection(for request: StateMachine.ConnectionRequest, in taskGroup: inout some TaskGroupProtocol) { - taskGroup.addTask_ { + self.addTask(into: &taskGroup) { self.observabilityDelegate.startedConnecting(id: request.connectionID) do { - let bundle = try await self.factory(request.connectionID, self) + let bundle = try await self.factory(request.connectionID, self.connectionConfiguration, self) self.connectionEstablished(bundle) // after the connection has been established, we keep the task open. This ensures @@ -474,7 +501,7 @@ public final class ConnectionPool< /*private*/ func runKeepAlive(_ connection: Connection, in taskGroup: inout some TaskGroupProtocol) { self.observabilityDelegate.keepAliveTriggered(id: connection.id) - taskGroup.addTask_ { + self.addTask(into: &taskGroup) { do { try await self.keepAliveBehavior.runKeepAlive(for: connection) @@ -508,8 +535,8 @@ public final class ConnectionPool< } @inlinable - /*private*/ func runTimer(_ timer: StateMachine.Timer, in poolGroup: inout some TaskGroupProtocol) { - poolGroup.addTask_ { () async -> () in + /*private*/ func runTimer(_ timer: StateMachine.Timer, in taskGroup: inout some TaskGroupProtocol) { + self.addTask(into: &taskGroup) { () async -> () in await withTaskGroup(of: TimerRunResult.self, returning: Void.self) { taskGroup in taskGroup.addTask { do { @@ -560,6 +587,17 @@ public final class ConnectionPool< token.resume() } } + + @inlinable + func addTask(into taskGroup: inout some TaskGroupProtocol, operation: @escaping @Sendable () async -> Void) { + #if compiler(>=6.0) + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *), let executor = self.executor as? TaskExecutor { + taskGroup.addTask_(executorPreference: executor, operation: operation) + return + } + #endif + taskGroup.addTask_(operation: operation) + } } @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @@ -579,6 +617,11 @@ protocol TaskGroupProtocol { // under exactly this name and others have different attributes. So let's pick // a name that doesn't clash anywhere and implement it using the standard `addTask`. mutating func addTask_(operation: @escaping @Sendable () async -> Void) + + #if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *) + mutating func addTask_(executorPreference: ((any TaskExecutor)?), operation: @escaping @Sendable () async -> Void) + #endif } @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) @@ -587,6 +630,14 @@ extension DiscardingTaskGroup: TaskGroupProtocol { mutating func addTask_(operation: @escaping @Sendable () async -> Void) { self.addTask(priority: nil, operation: operation) } + + #if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *) + @inlinable + mutating func addTask_(executorPreference: (any TaskExecutor)?, operation: @escaping @Sendable () async -> Void) { + self.addTask(executorPreference: executorPreference, operation: operation) + } + #endif } extension TaskGroup: TaskGroupProtocol { @@ -594,4 +645,12 @@ extension TaskGroup: TaskGroupProtocol { mutating func addTask_(operation: @escaping @Sendable () async -> Void) { self.addTask(priority: nil, operation: operation) } + + #if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *) + @inlinable + mutating func addTask_(executorPreference: (any TaskExecutor)?, operation: @escaping @Sendable () async -> Void) { + self.addTask(executorPreference: executorPreference, operation: operation) + } + #endif } diff --git a/Sources/ConnectionPoolModule/ConnectionPoolExecutor.swift b/Sources/ConnectionPoolModule/ConnectionPoolExecutor.swift new file mode 100644 index 00000000..b887752a --- /dev/null +++ b/Sources/ConnectionPoolModule/ConnectionPoolExecutor.swift @@ -0,0 +1,17 @@ +public protocol ConnectionPoolExecutor: AnyObject, Sendable { + associatedtype ID: Hashable, Sendable + + var id: ID { get } + + static func getExecutorID() -> Self.ID? +} + +public final class NothingConnectionPoolExecutor: ConnectionPoolExecutor { + public typealias ID = ObjectIdentifier + + public init() {} + + public var id: ObjectIdentifier { ObjectIdentifier(self) } + + public static func getExecutorID() -> ObjectIdentifier? { nil } +} diff --git a/Sources/ConnectionPoolModule/ConnectionPoolManager.swift b/Sources/ConnectionPoolModule/ConnectionPoolManager.swift new file mode 100644 index 00000000..81f0aaa2 --- /dev/null +++ b/Sources/ConnectionPoolModule/ConnectionPoolManager.swift @@ -0,0 +1,228 @@ +import Atomics + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) +public struct ConnectionPoolManagerConfiguration: Sendable { + /// The minimum number of connections to preserve in the pool. + /// + /// If the pool is mostly idle and the remote servers closes + /// idle connections, + /// the `ConnectionPool` will initiate new outbound + /// connections proactively to avoid the number of available + /// connections dropping below this number. + public var minimumConnectionPerExecutorCount: Int + + /// Between the `minimumConnectionCount` and + /// `maximumConnectionSoftLimit` the connection pool creates + /// _preserved_ connections. Preserved connections are closed + /// if they have been idle for ``idleTimeout``. + public var maximumConnectionPerExecutorSoftLimit: Int + + /// The maximum number of connections for this pool, that can + /// exist at any point in time. The pool can create _overflow_ + /// connections, if all connections are leased, and the + /// `maximumConnectionHardLimit` > `maximumConnectionSoftLimit ` + /// Overflow connections are closed immediately as soon as they + /// become idle. + public var maximumConnectionPerExecutorHardLimit: Int + + /// The time that a _preserved_ idle connection stays in the + /// pool before it is closed. + public var idleTimeout: Duration + + /// initializer + public init() { + self.minimumConnectionPerExecutorCount = 0 + self.maximumConnectionPerExecutorSoftLimit = 16 + self.maximumConnectionPerExecutorHardLimit = 16 + self.idleTimeout = .seconds(60) + } +} + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) +public final class ConnectionPoolManager< + Connection: PooledConnection, + ConnectionID: Hashable & Sendable, + ConnectionIDGenerator: ConnectionIDGeneratorProtocol, + ConnectionConfiguration: Equatable & Sendable, + Request: ConnectionRequestProtocol, + RequestID: Hashable & Sendable, + KeepAliveBehavior: ConnectionKeepAliveBehavior, + Executor: ConnectionPoolExecutor, + ObservabilityDelegate: ConnectionPoolObservabilityDelegate, + Clock: _Concurrency.Clock +>: Sendable where + Connection.ID == ConnectionID, + ConnectionIDGenerator.ID == ConnectionID, + Request.Connection == Connection, + Request.ID == RequestID, + KeepAliveBehavior.Connection == Connection, + ObservabilityDelegate.ConnectionID == ConnectionID, + Clock.Duration == Duration +{ + public typealias ConnectionFactory = @Sendable (ConnectionID, ConnectionConfiguration, ConnectionPool) async throws -> ConnectionAndMetadata + + public typealias ConnectionPool = _ConnectionPoolModule.ConnectionPool< + Connection, + ConnectionID, + ConnectionIDGenerator, + ConnectionConfiguration, + Request, + RequestID, + KeepAliveBehavior, + Executor, + ObservabilityDelegate, + Clock + > + + @usableFromInline + let pools: [Executor.ID: ConnectionPool] + + @usableFromInline + let roundRobinCounter = ManagedAtomic(0) + + @usableFromInline + let roundRobinPools: [ConnectionPool] + + @usableFromInline + let actionsStream: AsyncStream + + @usableFromInline + let eventContinuation: AsyncStream.Continuation + + @inlinable + public init( + configuration: ConnectionPoolManagerConfiguration, + connectionConfiguration: ConnectionConfiguration, + idGenerator: ConnectionIDGenerator, + requestType: Request.Type, + keepAliveBehavior: KeepAliveBehavior, + executors: [Executor], + observabilityDelegate: ObservabilityDelegate, + clock: Clock, + connectionFactory: @escaping ConnectionFactory + ) { + let (stream, continuation) = AsyncStream.makeStream(of: Actions.self) + self.actionsStream = stream + self.eventContinuation = continuation + + var pools = [Executor.ID: ConnectionPool]() + pools.reserveCapacity(executors.count) + + var singlePoolConfig = ConnectionPoolConfiguration() + singlePoolConfig.minimumConnectionCount = configuration.minimumConnectionPerExecutorCount + singlePoolConfig.maximumConnectionSoftLimit = configuration.maximumConnectionPerExecutorSoftLimit + singlePoolConfig.maximumConnectionHardLimit = configuration.maximumConnectionPerExecutorHardLimit + + for executor in executors { + pools[executor.id] = ConnectionPool( + configuration: singlePoolConfig, + connectionConfiguration: connectionConfiguration, + idGenerator: idGenerator, + requestType: requestType, + keepAliveBehavior: keepAliveBehavior, + executor: executor, + observabilityDelegate: observabilityDelegate, + clock: clock, + connectionFactory: connectionFactory + ) + } + + self.pools = pools + self.roundRobinPools = Array(pools.values) + + for pool in pools.values { + self.eventContinuation.yield(.runPool(pool)) + } + } + + @inlinable + public func leaseConnection(_ request: Request) { + if let executorID = Executor.getExecutorID(), let pool = self.pools[executorID] { + pool.leaseConnection(request) + return + } + + let index = self.roundRobinCounter.loadThenWrappingIncrement(ordering: .relaxed) % self.roundRobinPools.count + self.roundRobinPools[index].leaseConnection(request) + } + + @inlinable + public func cancelLeaseConnection(_ requestID: RequestID) { + // TODO: This is expensive! + for pool in self.roundRobinPools { + if pool.cancelLeaseConnection(requestID) { + break + } + } + } + + @usableFromInline + enum Actions: Sendable { + case runPool(ConnectionPool) + } + + @inlinable + public func run() async { + await withTaskCancellationHandler { + if #available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) { + return await withDiscardingTaskGroup() { taskGroup in + await self.run(in: &taskGroup) + } + } + return await withTaskGroup(of: Void.self) { taskGroup in + await self.run(in: &taskGroup) + } + } onCancel: { + + } + } + + @inlinable + public func updateConfiguration(_ configuration: ConnectionConfiguration, forceReconnection: Bool) { + for pool in self.pools.values { + pool.updateConfiguration(configuration, forceReconnection: forceReconnection) + } + } + + // MARK: - Private Methods - + + @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) + @inlinable + /* private */ func run(in taskGroup: inout DiscardingTaskGroup) async { + for await event in self.actionsStream { + self.runEvent(event, in: &taskGroup) + } + } + + @inlinable + /* private */ func run(in taskGroup: inout TaskGroup) async { + var running = 0 + for await event in self.actionsStream { + running += 1 + self.runEvent(event, in: &taskGroup) + + if running == 100 { + _ = await taskGroup.next() + running -= 1 + } + } + } + + @inlinable + /* private */ func runEvent(_ event: Actions, in taskGroup: inout some TaskGroupProtocol) { + switch event { + case .runPool(let pool): + #if compiler(>=6.0) + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *), let executor = pool.executor as? TaskExecutor { + taskGroup.addTask_(executorPreference: executor) { + await pool.run() + } + return + } + #endif + taskGroup.addTask_ { + await pool.run() + } + } + } +} diff --git a/Sources/ConnectionPoolModule/ConnectionRequest.swift b/Sources/ConnectionPoolModule/ConnectionRequest.swift index d6654a27..e58bac95 100644 --- a/Sources/ConnectionPoolModule/ConnectionRequest.swift +++ b/Sources/ConnectionPoolModule/ConnectionRequest.swift @@ -1,3 +1,4 @@ +import Atomics public struct ConnectionRequest: ConnectionRequestProtocol { public typealias ID = Int @@ -28,17 +29,21 @@ let requestIDGenerator = _ConnectionPoolModule.ConnectionIDGenerator() extension ConnectionPool where Request == ConnectionRequest { public convenience init( configuration: ConnectionPoolConfiguration, + connectionConfiguration: ConnectionConfiguration, idGenerator: ConnectionIDGenerator = _ConnectionPoolModule.ConnectionIDGenerator(), keepAliveBehavior: KeepAliveBehavior, + executor: Executor, observabilityDelegate: ObservabilityDelegate, clock: Clock = ContinuousClock(), connectionFactory: @escaping ConnectionFactory ) { self.init( configuration: configuration, + connectionConfiguration: connectionConfiguration, idGenerator: idGenerator, requestType: ConnectionRequest.self, keepAliveBehavior: keepAliveBehavior, + executor: executor, observabilityDelegate: observabilityDelegate, clock: clock, connectionFactory: connectionFactory @@ -76,3 +81,21 @@ extension ConnectionPool where Request == ConnectionRequest { return try await closure(lease.connection) } } + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) +extension ConnectionPoolManager where Request == ConnectionRequest { + @inlinable + public func leaseConnection() async throws -> ConnectionLease { + + let index = self.roundRobinCounter.loadThenWrappingIncrement(ordering: .relaxed) % self.roundRobinPools.count + + return try await self.roundRobinPools[index].leaseConnection() + } + + @inlinable + public func withConnection(_ closure: (Connection) async throws -> Result) async throws -> Result { + let lease = try await self.leaseConnection() + defer { lease.release() } + return try await closure(lease.connection) + } +} diff --git a/Sources/ConnectionPoolTestUtils/MockConnection.swift b/Sources/ConnectionPoolTestUtils/MockConnection.swift index db5c3ef7..5691a28e 100644 --- a/Sources/ConnectionPoolTestUtils/MockConnection.swift +++ b/Sources/ConnectionPoolTestUtils/MockConnection.swift @@ -2,7 +2,17 @@ import _ConnectionPoolModule import DequeModule import NIOConcurrencyHelpers -public final class MockConnection: PooledConnection, Sendable { +public struct MockConnectionConfiguration: Sendable, Hashable { + public var username: String + public var password: String + + public init(username: String, password: String) { + self.username = username + self.password = password + } +} + +public final class MockConnection: PooledConnection, Sendable { public typealias ID = Int public let id: ID @@ -15,7 +25,7 @@ public final class MockConnection: PooledConnection, Sendable { private let lock: NIOLockedValueBox = NIOLockedValueBox(.running([], [])) - public init(id: Int) { + public init(id: Int, executor: Executor) { self.id = id } diff --git a/Sources/ConnectionPoolTestUtils/MockConnectionFactory.swift b/Sources/ConnectionPoolTestUtils/MockConnectionFactory.swift index 936b47cc..91a1aad2 100644 --- a/Sources/ConnectionPoolTestUtils/MockConnectionFactory.swift +++ b/Sources/ConnectionPoolTestUtils/MockConnectionFactory.swift @@ -3,24 +3,30 @@ import DequeModule import NIOConcurrencyHelpers @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) -public final class MockConnectionFactory: Sendable where Clock.Duration == Duration { +public final class MockConnectionFactory: Sendable where Clock.Duration == Duration { public typealias ConnectionIDGenerator = _ConnectionPoolModule.ConnectionIDGenerator - public typealias Request = ConnectionRequest + public typealias Request = ConnectionRequest> public typealias KeepAliveBehavior = MockPingPongBehavior public typealias MetricsDelegate = NoOpConnectionPoolMetrics public typealias ConnectionID = Int public typealias Connection = MockConnection + @usableFromInline let stateBox = NIOLockedValueBox(State()) - struct State { - var attempts = Deque<(ConnectionID, CheckedContinuation<(MockConnection, UInt16), any Error>)>() + @usableFromInline + struct State: Sendable { + @usableFromInline + var attempts = Deque<(ConnectionID, Executor, CheckedContinuation<(MockConnection, UInt16), any Error>)>() - var waiter = Deque), Never>>() + @usableFromInline + var waiter = Deque, UInt16), any Error>), Never>>() - var runningConnections = [ConnectionID: Connection]() + @usableFromInline + var runningConnections = [ConnectionID: Connection]() } + @usableFromInline let autoMaxStreams: UInt16? public init(autoMaxStreams: UInt16? = nil) { @@ -31,16 +37,18 @@ public final class MockConnectionFactory: Sendable wh self.stateBox.withLockedValue { $0.attempts.count } } - public var runningConnections: [Connection] { + public var runningConnections: [Connection] { self.stateBox.withLockedValue { Array($0.runningConnections.values) } } + @inlinable public func makeConnection( id: Int, - for pool: ConnectionPool, NoOpConnectionPoolMetrics, Clock> - ) async throws -> ConnectionAndMetadata { + configuration: MockConnectionConfiguration, + for pool: ConnectionPool, Int, ConnectionIDGenerator, MockConnectionConfiguration, some ConnectionRequestProtocol, Int, MockPingPongBehavior>, Executor, NoOpConnectionPoolMetrics, Clock> + ) async throws -> ConnectionAndMetadata> { if let autoMaxStreams = self.autoMaxStreams { - let connection = MockConnection(id: id) + let connection = MockConnection(id: id, executor: pool.executor) Task { try? await connection.signalToClose connection.closeIfClosing() @@ -49,18 +57,18 @@ public final class MockConnectionFactory: Sendable wh } // we currently don't support cancellation when creating a connection - let result = try await withCheckedThrowingContinuation { (checkedContinuation: CheckedContinuation<(MockConnection, UInt16), any Error>) in - let waiter = self.stateBox.withLockedValue { state -> (CheckedContinuation<(ConnectionID, CheckedContinuation<(MockConnection, UInt16), any Error>), Never>)? in + let result = try await withCheckedThrowingContinuation { (checkedContinuation: CheckedContinuation<(MockConnection, UInt16), any Error>) in + let waiter = self.stateBox.withLockedValue { state -> (CheckedContinuation<(ConnectionID, Executor, CheckedContinuation<(MockConnection, UInt16), any Error>), Never>)? in if let waiter = state.waiter.popFirst() { return waiter } else { - state.attempts.append((id, checkedContinuation)) + state.attempts.append((id, pool.executor, checkedContinuation)) return nil } } if let waiter { - waiter.resume(returning: (id, checkedContinuation)) + waiter.resume(returning: (id, pool.executor, checkedContinuation)) } } @@ -68,9 +76,10 @@ public final class MockConnectionFactory: Sendable wh } @discardableResult - public func nextConnectAttempt(_ closure: (ConnectionID) async throws -> UInt16) async rethrows -> Connection { - let (connectionID, continuation) = await withCheckedContinuation { (continuation: CheckedContinuation<(ConnectionID, CheckedContinuation<(MockConnection, UInt16), any Error>), Never>) in - let attempt = self.stateBox.withLockedValue { state -> (ConnectionID, CheckedContinuation<(MockConnection, UInt16), any Error>)? in + @inlinable + public func nextConnectAttempt(_ closure: (ConnectionID) async throws -> UInt16) async rethrows -> Connection { + let (connectionID, executor, continuation) = await withCheckedContinuation { (continuation: CheckedContinuation<(ConnectionID, Executor, CheckedContinuation<(MockConnection, UInt16), any Error>), Never>) in + let attempt = self.stateBox.withLockedValue { state -> (ConnectionID, Executor, CheckedContinuation<(MockConnection, UInt16), any Error>)? in if let attempt = state.attempts.popFirst() { return attempt } else { @@ -86,7 +95,7 @@ public final class MockConnectionFactory: Sendable wh do { let streamCount = try await closure(connectionID) - let connection = MockConnection(id: connectionID) + let connection = MockConnection(id: connectionID, executor: executor) connection.onClose { _ in self.stateBox.withLockedValue { state in diff --git a/Sources/ConnectionPoolTestUtils/MockExecutor.swift b/Sources/ConnectionPoolTestUtils/MockExecutor.swift new file mode 100644 index 00000000..f1b93956 --- /dev/null +++ b/Sources/ConnectionPoolTestUtils/MockExecutor.swift @@ -0,0 +1,23 @@ +// +// MockExecutor.swift +// postgres-nio +// +// Created by Fabian Fett on 07.05.25. +// + +import _ConnectionPoolModule + +public final class MockExecutor: ConnectionPoolExecutor, Sendable { + public typealias ID = ObjectIdentifier + + public var id: ID { ObjectIdentifier(self) } + + static public func getExecutorID() -> ObjectIdentifier? { + MockExecutor.executorID + } + + public init() {} + + @TaskLocal + static var executorID: MockExecutor.ID? +} diff --git a/Sources/ConnectionPoolTestUtils/MockRequest.swift b/Sources/ConnectionPoolTestUtils/MockRequest.swift index 3dd8b0fb..1831f2a1 100644 --- a/Sources/ConnectionPoolTestUtils/MockRequest.swift +++ b/Sources/ConnectionPoolTestUtils/MockRequest.swift @@ -21,6 +21,7 @@ public final class MockRequest: ConnectionRequestP hasher.combine(self.id) } + @inlinable public func complete(with: Result, ConnectionPoolError>) { } diff --git a/Sources/PostgresNIO/Pool/ConnectionFactory.swift b/Sources/PostgresNIO/Pool/ConnectionFactory.swift index 31343826..26e11f34 100644 --- a/Sources/PostgresNIO/Pool/ConnectionFactory.swift +++ b/Sources/PostgresNIO/Pool/ConnectionFactory.swift @@ -35,7 +35,7 @@ final class ConnectionFactory: Sendable { self.logger = logger } - func makeConnection(_ connectionID: PostgresConnection.ID, pool: PostgresClient.Pool) async throws -> PostgresConnection { + func makeConnection(_ connectionID: PostgresConnection.ID, pool: PostgresClient.ConnectionPool) async throws -> PostgresConnection { let config = try await self.makeConnectionConfig() var connectionLogger = self.logger diff --git a/Sources/PostgresNIO/Pool/PostgresClient.swift b/Sources/PostgresNIO/Pool/PostgresClient.swift index 581b5113..634f1697 100644 --- a/Sources/PostgresNIO/Pool/PostgresClient.swift +++ b/Sources/PostgresNIO/Pool/PostgresClient.swift @@ -233,17 +233,130 @@ public final class PostgresClient: Sendable, ServiceLifecycle.Service { } } - typealias Pool = ConnectionPool< + typealias ConnectionPoolManager = _ConnectionPoolModule.ConnectionPoolManager< PostgresConnection, PostgresConnection.ID, ConnectionIDGenerator, - ConnectionRequest, - ConnectionRequest.ID, + Foo, + PostgresConnectionRequest, + PostgresConnectionRequest.ID, PostgresKeepAliveBehavor, + NothingConnectionPoolExecutor, PostgresClientMetrics, ContinuousClock > + typealias ConnectionPool = _ConnectionPoolModule.ConnectionPool< + PostgresConnection, + PostgresConnection.ID, + ConnectionIDGenerator, + Foo, + PostgresConnectionRequest, + PostgresConnectionRequest.ID, + PostgresKeepAliveBehavor, + NothingConnectionPoolExecutor, + PostgresClientMetrics, + ContinuousClock + > + + enum Pool { + case manager(ConnectionPoolManager) + case pool(ConnectionPool) + + init( + configuration: Configuration, + factory: ConnectionFactory, + eventLoopGroup: any EventLoopGroup, + backgroundLogger: Logger + ) { + let idGenerator = ConnectionIDGenerator.globalGenerator + + if configuration.options.maximumConnections > 50 { + // make as many executors as we have NIO else + let executorCount = Int(ceil(Double(configuration.options.maximumConnections) / 50.0)) + let executors = (0.. ConnectionLease { + let requestID = PostgresConnectionRequest.idGenerator.next() + + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation, Error>) in + let request = PostgresConnectionRequest(id: requestID, continuation: continuation) + + self.leaseConnection(request) + } + } onCancel: { + self.cancelRequest(id: requestID) + } + } + } + let pool: Pool let factory: ConnectionFactory let runningAtomic = ManagedAtomic(false) @@ -280,18 +393,12 @@ public final class PostgresClient: Sendable, ServiceLifecycle.Service { self.factory = factory self.backgroundLogger = backgroundLogger - self.pool = ConnectionPool( - configuration: .init(configuration), - idGenerator: ConnectionIDGenerator(), - requestType: ConnectionRequest.self, - keepAliveBehavior: .init(configuration.options.keepAliveBehavior, logger: backgroundLogger), - observabilityDelegate: .init(logger: backgroundLogger), - clock: ContinuousClock() - ) { (connectionID, pool) in - let connection = try await factory.makeConnection(connectionID, pool: pool) - - return ConnectionAndMetadata(connection: connection, maximalStreamsOnConnection: 1) - } + self.pool = .init( + configuration: configuration, + factory: factory, + eventLoopGroup: eventLoopGroup, + backgroundLogger: backgroundLogger + ) } /// Lease a connection for the provided `closure`'s lifetime. @@ -374,30 +481,22 @@ public final class PostgresClient: Sendable, ServiceLifecycle.Service { throw PSQLError(code: .tooManyParameters, query: query, file: file, line: line) } - let lease = try await self.leaseConnection() - let connection = lease.connection + let requestID = PostgresConnectionRequest.idGenerator.next() - var logger = logger - logger[postgresMetadataKey: .connectionID] = "\(connection.id)" + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let request = PostgresConnectionRequest( + id: requestID, + query: query, + continuation: continuation, + logger: logger + ) - let promise = connection.channel.eventLoop.makePromise(of: PSQLRowStream.self) - let context = ExtendedQueryContext( - query: query, - logger: logger, - promise: promise - ) - - connection.channel.write(HandlerTask.extendedQuery(context), promise: nil) - - promise.futureResult.whenFailure { _ in - lease.release() + self.pool.leaseConnection(request) + } + } onCancel: { + self.pool.cancelRequest(id: requestID) } - - return try await promise.futureResult.map { - $0.asyncSequence(onFinish: { - lease.release() - }) - }.get() } catch var error as PSQLError { error.file = file error.line = line @@ -490,7 +589,11 @@ public final class PostgresClient: Sendable, ServiceLifecycle.Service { PostgresConnection.defaultEventLoopGroup } - static let loggingDisabled = Logger(label: "Postgres-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) + static let loggingDisabled = { + var logger = Logger(label: "Postgres-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) + logger.logLevel = .critical + return logger + }() } @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @@ -551,3 +654,5 @@ extension ConnectionPoolError { return psqlError } } + +struct Foo: Equatable {} diff --git a/Sources/PostgresNIO/Pool/PostgresConnectionRequest.swift b/Sources/PostgresNIO/Pool/PostgresConnectionRequest.swift new file mode 100644 index 00000000..48a6c140 --- /dev/null +++ b/Sources/PostgresNIO/Pool/PostgresConnectionRequest.swift @@ -0,0 +1,78 @@ +// +// PostgresConnectionRequest.swift +// postgres-nio +// +// Created by Fabian Fett on 15.05.25. +// + +import _ConnectionPoolModule + +struct PostgresConnectionRequest: ConnectionRequestProtocol { + + static let idGenerator = ConnectionIDGenerator() + + private enum `Type` { + case connection(CheckedContinuation, any Error>) + case query(PostgresQuery, Logger, CheckedContinuation) + } + + typealias ID = Int + + var id: ID + private var type: `Type` + + init( + id: Int, + continuation: CheckedContinuation, any Error> + ) { + self.id = id + self.type = .connection(continuation) + } + + init( + id: Int, + query: PostgresQuery, + continuation: CheckedContinuation, + logger: Logger + ) { + self.id = id + self.type = .query(query, logger, continuation) + } + + public func complete(with result: Result, ConnectionPoolError>) { + switch self.type { + case .connection(let checkedContinuation): + checkedContinuation.resume(with: result) + + case .query(let query, var logger, let checkedContinuation): + switch result { + case .success(let lease): + logger[postgresMetadataKey: .connectionID] = "\(lease.connection.id)" + + let promise = lease.connection.channel.eventLoop.makePromise(of: PSQLRowStream.self) + let context = ExtendedQueryContext( + query: query, + logger: logger, + promise: promise + ) + + lease.connection.channel.write(HandlerTask.extendedQuery(context), promise: nil) + promise.futureResult.whenFailure { error in + lease.release() + checkedContinuation.resume(throwing: error) + } + + promise.futureResult.whenSuccess { rowSequence in + let asyncSequence = rowSequence.asyncSequence { + lease.release() + } + checkedContinuation.resume(returning: asyncSequence) + } + + case .failure(let error): + checkedContinuation.resume(throwing: error) + } + } + + } +} diff --git a/Tests/ConnectionPoolModuleTests/ConnectionPoolTests.swift b/Tests/ConnectionPoolModuleTests/ConnectionPoolTests.swift index f3664242..5838e63a 100644 --- a/Tests/ConnectionPoolModuleTests/ConnectionPoolTests.swift +++ b/Tests/ConnectionPoolModuleTests/ConnectionPoolTests.swift @@ -4,25 +4,28 @@ import Atomics import NIOEmbedded import Testing - @Suite struct ConnectionPoolTests { + let executor = MockExecutor() + @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func test1000ConsecutiveRequestsOnSingleConnection() async { - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() var config = ConnectionPoolConfiguration() config.minimumConnectionCount = 1 let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionRequest.self, keepAliveBehavior: MockPingPongBehavior(keepAliveFrequency: nil, connectionType: MockConnection.self), - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: self.executor, + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: ContinuousClock() ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } // the same connection is reused 1000 times @@ -38,8 +41,8 @@ import Testing do { for _ in 0..<1000 { - async let connectionFuture = pool.leaseConnection() - var connectionLease: ConnectionLease? + async let connectionFuture = try await pool.leaseConnection() + var connectionLease: ConnectionLease>? #expect(factory.pendingConnectionAttemptsCount == 0) connectionLease = try await connectionFuture #expect(connectionLease != nil) @@ -65,20 +68,22 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testShutdownPoolWhileConnectionIsBeingCreated() async { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() var config = ConnectionPoolConfiguration() config.minimumConnectionCount = 1 let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionRequest.self, keepAliveBehavior: MockPingPongBehavior(keepAliveFrequency: nil, connectionType: MockConnection.self), - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } await withTaskGroup(of: Void.self) { taskGroup in @@ -111,20 +116,22 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testShutdownPoolWhileConnectionIsBackingOff() async { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() var config = ConnectionPoolConfiguration() config.minimumConnectionCount = 1 let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionRequest.self, keepAliveBehavior: MockPingPongBehavior(keepAliveFrequency: nil, connectionType: MockConnection.self), - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } await withTaskGroup(of: Void.self) { taskGroup in @@ -146,7 +153,7 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testConnectionHardLimitIsRespected() async { - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 0 @@ -156,13 +163,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionRequest.self, keepAliveBehavior: MockPingPongBehavior(keepAliveFrequency: nil, connectionType: MockConnection.self), - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: ContinuousClock() ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } let hasFinished = ManagedAtomic(false) @@ -226,9 +235,9 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testKeepAliveWorks() async throws { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() let keepAliveDuration = Duration.seconds(30) - let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) + let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 0 @@ -238,13 +247,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionRequest.self, keepAliveBehavior: keepAlive, - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -306,9 +317,9 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testKeepAliveOnClose() async throws { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() let keepAliveDuration = Duration.seconds(20) - let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) + let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 0 @@ -318,13 +329,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionRequest.self, keepAliveBehavior: keepAlive, - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -393,9 +406,9 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testKeepAliveWorksRacesAgainstShutdown() async throws { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() let keepAliveDuration = Duration.seconds(30) - let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) + let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 0 @@ -405,13 +418,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionRequest.self, keepAliveBehavior: keepAlive, - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -451,7 +466,6 @@ import Testing } taskGroup.cancelAll() - print("cancelled") for connection in factory.runningConnections { connection.closeIfClosing() @@ -462,9 +476,9 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testCancelConnectionRequestWorks() async throws { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() let keepAliveDuration = Duration.seconds(30) - let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) + let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 0 @@ -475,13 +489,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionRequest.self, keepAliveBehavior: keepAlive, - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -523,9 +539,9 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testLeasingMultipleConnectionsAtOnceWorks() async throws { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() let keepAliveDuration = Duration.seconds(30) - let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) + let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 4 @@ -536,13 +552,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionFuture.self, keepAliveBehavior: keepAlive, - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -562,7 +580,7 @@ import Testing // lease 4 connections at once pool.leaseConnections(requests) - var connectionLeases = [ConnectionLease]() + var connectionLeases = [ConnectionLease>]() for request in requests { let connection = try await request.future.success @@ -588,9 +606,9 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testLeasingConnectionAfterShutdownIsInvokedFails() async throws { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() let keepAliveDuration = Duration.seconds(30) - let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) + let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 4 @@ -601,13 +619,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionRequest.self, keepAliveBehavior: keepAlive, - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -644,9 +664,9 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testLeasingConnectionsAfterShutdownIsInvokedFails() async throws { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() let keepAliveDuration = Duration.seconds(30) - let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) + let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 4 @@ -657,13 +677,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionFuture.self, keepAliveBehavior: keepAlive, - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -706,9 +728,9 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testLeasingMultipleStreamsFromOneConnectionWorks() async throws { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() let keepAliveDuration = Duration.seconds(30) - let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) + let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 0 @@ -719,13 +741,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionFuture.self, keepAliveBehavior: keepAlive, - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -736,7 +760,7 @@ import Testing // create 4 connection requests let requests = (0..<10).map { ConnectionFuture(id: $0) } pool.leaseConnections(requests) - var connectionLeases = [ConnectionLease]() + var connectionLeases = [ConnectionLease>]() await factory.nextConnectAttempt { connectionID in return 10 @@ -772,9 +796,9 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testIncreasingAvailableStreamsWorks() async throws { let clock = MockClock() - let factory = MockConnectionFactory() + let factory = MockConnectionFactory() let keepAliveDuration = Duration.seconds(30) - let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) + let keepAlive = MockPingPongBehavior(keepAliveFrequency: keepAliveDuration, connectionType: MockConnection.self) var mutableConfig = ConnectionPoolConfiguration() mutableConfig.minimumConnectionCount = 0 @@ -785,13 +809,15 @@ import Testing let pool = ConnectionPool( configuration: config, + connectionConfiguration: MockConnectionConfiguration(username: "username", password: "password"), idGenerator: ConnectionIDGenerator(), requestType: ConnectionFuture.self, keepAliveBehavior: keepAlive, - observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), + executor: MockExecutor(), + observabilityDelegate: NoOpConnectionPoolMetrics(connectionIDType: MockConnection.ID.self), clock: clock ) { - try await factory.makeConnection(id: $0, for: $1) + try await factory.makeConnection(id: $0, configuration: $1, for: $2) } try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -802,7 +828,7 @@ import Testing // create 4 connection requests var requests = (0..<21).map { ConnectionFuture(id: $0) } pool.leaseConnections(requests) - var connectionLease = [ConnectionLease]() + var connectionLease = [ConnectionLease>]() await factory.nextConnectAttempt { connectionID in return 1 @@ -850,14 +876,14 @@ import Testing struct ConnectionFuture: ConnectionRequestProtocol { let id: Int - let future: Future> + let future: Future>> init(id: Int) { self.id = id self.future = Future(of: ConnectionLease.self) } - func complete(with result: Result, ConnectionPoolError>) { + func complete(with result: Result>, ConnectionPoolError>) { switch result { case .success(let success): self.future.yield(value: success) diff --git a/Tests/ConnectionPoolModuleTests/ConnectionRequestTests.swift b/Tests/ConnectionPoolModuleTests/ConnectionRequestTests.swift index b4658df8..f054e451 100644 --- a/Tests/ConnectionPoolModuleTests/ConnectionRequestTests.swift +++ b/Tests/ConnectionPoolModuleTests/ConnectionRequestTests.swift @@ -4,9 +4,12 @@ import Testing @Suite struct ConnectionRequestTests { + let executor = NothingConnectionPoolExecutor() + @Test func testHappyPath() async throws { - let mockConnection = MockConnection(id: 1) - let lease = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation, any Error>) in + let mockConnection = MockConnection(id: 1, executor: self.executor) + let lease = try await withCheckedThrowingContinuation { + (continuation: CheckedContinuation>, any Error>) in let request = ConnectionRequest(id: 42, continuation: continuation) #expect(request.id == 42) let lease = ConnectionLease(connection: mockConnection) { _ in } @@ -18,7 +21,8 @@ import Testing @Test func testSadPath() async throws { do { - _ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + _ = try await withCheckedThrowingContinuation { + (continuation: CheckedContinuation, any Error>) in continuation.resume(with: .failure(ConnectionPoolError.requestCancelled)) } Issue.record("This point should not be reached") diff --git a/Tests/ConnectionPoolModuleTests/NoKeepAliveBehaviorTests.swift b/Tests/ConnectionPoolModuleTests/NoKeepAliveBehaviorTests.swift index ef6b001a..19945d10 100644 --- a/Tests/ConnectionPoolModuleTests/NoKeepAliveBehaviorTests.swift +++ b/Tests/ConnectionPoolModuleTests/NoKeepAliveBehaviorTests.swift @@ -2,11 +2,10 @@ import _ConnectionPoolModule import _ConnectionPoolTestUtils import Testing - @Suite struct NoKeepAliveBehaviorTests { @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testNoKeepAlive() { - let keepAliveBehavior = NoOpKeepAliveBehavior(connectionType: MockConnection.self) + let keepAliveBehavior = NoOpKeepAliveBehavior(connectionType: MockConnection.self) #expect(keepAliveBehavior.keepAliveFrequency == nil) } } diff --git a/Tests/ConnectionPoolModuleTests/PoolStateMachine+ConnectionGroupTests.swift b/Tests/ConnectionPoolModuleTests/PoolStateMachine+ConnectionGroupTests.swift index 6bfe0f39..a21d2085 100644 --- a/Tests/ConnectionPoolModuleTests/PoolStateMachine+ConnectionGroupTests.swift +++ b/Tests/ConnectionPoolModuleTests/PoolStateMachine+ConnectionGroupTests.swift @@ -4,6 +4,7 @@ import Testing @Suite struct PoolStateMachine_ConnectionGroupTests { var idGenerator = ConnectionIDGenerator() + let executor = NothingConnectionPoolExecutor() @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testRefillConnections() { @@ -31,7 +32,7 @@ import Testing var connected: UInt16 = 0 for request in requests { - let newConnection = MockConnection(id: request.connectionID) + let newConnection = MockConnection(id: request.connectionID, executor: self.executor) let (_, context) = connections.newConnectionEstablished(newConnection, maxStreams: 1) #expect(context.info == .idle(availableStreams: 1, newIdle: true)) #expect(context.use == .persisted) @@ -68,7 +69,7 @@ import Testing #expect(connections.soonAvailableConnections == 1) #expect(connections.stats == .init(connecting: 1)) - let newConnection = MockConnection(id: request.connectionID) + let newConnection = MockConnection(id: request.connectionID, executor: self.executor) let (_, establishedContext) = connections.newConnectionEstablished(newConnection, maxStreams: 1) #expect(establishedContext.info == .idle(availableStreams: 1, newIdle: true)) #expect(establishedContext.use == .demand) @@ -188,14 +189,14 @@ import Testing } #expect(connections.stats == .init(connecting: 3)) - let newSecondConnection = MockConnection(id: secondRequest.connectionID) + let newSecondConnection = MockConnection(id: secondRequest.connectionID, executor: self.executor) let (_, establishedSecondConnectionContext) = connections.newConnectionEstablished(newSecondConnection, maxStreams: 1) #expect(establishedSecondConnectionContext.info == .idle(availableStreams: 1, newIdle: true)) #expect(establishedSecondConnectionContext.use == .persisted) #expect(connections.stats == .init(connecting: 2, idle: 1, availableStreams: 1)) #expect(connections.soonAvailableConnections == 2) - let newThirdConnection = MockConnection(id: thirdRequest.connectionID) + let newThirdConnection = MockConnection(id: thirdRequest.connectionID, executor: self.executor) let (thirdConnectionIndex, establishedThirdConnectionContext) = connections.newConnectionEstablished(newThirdConnection, maxStreams: 1) #expect(establishedThirdConnectionContext.info == .idle(availableStreams: 1, newIdle: true)) #expect(establishedThirdConnectionContext.use == .demand) @@ -245,7 +246,7 @@ import Testing } #expect(connections.stats == .init(connecting: 2)) - let newFirstConnection = MockConnection(id: firstRequest.connectionID) + let newFirstConnection = MockConnection(id: firstRequest.connectionID, executor: self.executor) let (_, establishedFirstConnectionContext) = connections.newConnectionEstablished(newFirstConnection, maxStreams: 1) #expect(establishedFirstConnectionContext.info == .idle(availableStreams: 1, newIdle: true)) #expect(establishedFirstConnectionContext.use == .demand) @@ -284,7 +285,7 @@ import Testing return } - let newConnection = MockConnection(id: firstRequest.connectionID) + let newConnection = MockConnection(id: firstRequest.connectionID, executor: self.executor) let (connectionIndex, establishedConnectionContext) = connections.newConnectionEstablished(newConnection, maxStreams: 1) #expect(establishedConnectionContext.info == .idle(availableStreams: 1, newIdle: true)) #expect(establishedConnectionContext.use == .persisted) @@ -323,7 +324,7 @@ import Testing return } - let newConnection = MockConnection(id: firstRequest.connectionID) + let newConnection = MockConnection(id: firstRequest.connectionID, executor: self.executor) let (connectionIndex, establishedConnectionContext) = connections.newConnectionEstablished(newConnection, maxStreams: 1) #expect(establishedConnectionContext.info == .idle(availableStreams: 1, newIdle: true)) #expect(connections.stats == .init(idle: 1, availableStreams: 1)) diff --git a/Tests/ConnectionPoolModuleTests/PoolStateMachine+ConnectionStateTests.swift b/Tests/ConnectionPoolModuleTests/PoolStateMachine+ConnectionStateTests.swift index 2d81cf38..ab0d4f6f 100644 --- a/Tests/ConnectionPoolModuleTests/PoolStateMachine+ConnectionStateTests.swift +++ b/Tests/ConnectionPoolModuleTests/PoolStateMachine+ConnectionStateTests.swift @@ -6,6 +6,8 @@ import Testing typealias TestConnectionState = TestPoolStateMachine.ConnectionState + let executor = NothingConnectionPoolExecutor() + @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testStartupLeaseReleaseParkLease() { let connectionID = 1 @@ -15,7 +17,7 @@ import Testing #expect(!state.isAvailable) #expect(!state.isConnected) #expect(!state.isLeased) - let connection = MockConnection(id: connectionID) + let connection = MockConnection(id: connectionID, executor: self.executor) #expect(state.connected(connection, maxStreams: 1) == .idle(availableStreams: 1, newIdle: true)) #expect(state.isIdle) #expect(state.isAvailable) @@ -60,7 +62,7 @@ import Testing @Test func testStartupParkLeaseBeforeTimersRegistered() { let connectionID = 1 var state = TestConnectionState(id: connectionID) - let connection = MockConnection(id: connectionID) + let connection = MockConnection(id: connectionID, executor: self.executor) #expect(state.connected(connection, maxStreams: 1) == .idle(availableStreams: 1, newIdle: true)) let parkResult = state.parkConnection(scheduleKeepAliveTimer: true, scheduleIdleTimeoutTimer: true) #expect( @@ -88,7 +90,7 @@ import Testing @Test func testStartupParkLeasePark() { let connectionID = 1 var state = TestConnectionState(id: connectionID) - let connection = MockConnection(id: connectionID) + let connection = MockConnection(id: connectionID, executor: self.executor) #expect(state.connected(connection, maxStreams: 1) == .idle(availableStreams: 1, newIdle: true)) let parkResult = state.parkConnection(scheduleKeepAliveTimer: true, scheduleIdleTimeoutTimer: true) #expect( @@ -151,7 +153,7 @@ import Testing ) #expect(state.retryConnect() == forthBackoffTimerCancellationToken) - let connection = MockConnection(id: connectionID) + let connection = MockConnection(id: connectionID, executor: self.executor) #expect(state.connected(connection, maxStreams: 1) == .idle(availableStreams: 1, newIdle: true)) } @@ -159,7 +161,7 @@ import Testing @Test func testLeaseMultipleStreams() { let connectionID = 1 var state = TestConnectionState(id: connectionID) - let connection = MockConnection(id: connectionID) + let connection = MockConnection(id: connectionID, executor: self.executor) #expect(state.connected(connection, maxStreams: 100) == .idle(availableStreams: 100, newIdle: true)) let timers = state.parkConnection(scheduleKeepAliveTimer: true, scheduleIdleTimeoutTimer: false) guard let keepAliveTimer = timers.first else { @@ -196,7 +198,7 @@ import Testing @Test func testRunningKeepAliveReducesAvailableStreams() { let connectionID = 1 var state = TestConnectionState(id: connectionID) - let connection = MockConnection(id: connectionID) + let connection = MockConnection(id: connectionID, executor: self.executor) #expect(state.connected(connection, maxStreams: 100) == .idle(availableStreams: 100, newIdle: true)) let timers = state.parkConnection(scheduleKeepAliveTimer: true, scheduleIdleTimeoutTimer: false) guard let keepAliveTimer = timers.first else { @@ -228,11 +230,10 @@ import Testing #expect(state.isAvailable) } - @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) - @Test func testRunningKeepAliveDoesNotReduceAvailableStreams() { + func testRunningKeepAliveDoesNotReduceAvailableStreams() { let connectionID = 1 var state = TestConnectionState(id: connectionID) - let connection = MockConnection(id: connectionID) + let connection = MockConnection(id: connectionID, executor: self.executor) #expect(state.connected(connection, maxStreams: 100) == .idle(availableStreams: 100, newIdle: true)) let timers = state.parkConnection(scheduleKeepAliveTimer: true, scheduleIdleTimeoutTimer: false) guard let keepAliveTimer = timers.first else { @@ -261,7 +262,7 @@ import Testing @Test func testRunKeepAliveRacesAgainstIdleClose() { let connectionID = 1 var state = TestConnectionState(id: connectionID) - let connection = MockConnection(id: connectionID) + let connection = MockConnection(id: connectionID, executor: self.executor) #expect(state.connected(connection, maxStreams: 1) == .idle(availableStreams: 1, newIdle: true)) let parkResult = state.parkConnection(scheduleKeepAliveTimer: true, scheduleIdleTimeoutTimer: true) guard let keepAliveTimer = parkResult.first, let idleTimer = parkResult.second else { diff --git a/Tests/ConnectionPoolModuleTests/PoolStateMachine+RequestQueueTests.swift b/Tests/ConnectionPoolModuleTests/PoolStateMachine+RequestQueueTests.swift index 458c6b3f..0cc31c3d 100644 --- a/Tests/ConnectionPoolModuleTests/PoolStateMachine+RequestQueueTests.swift +++ b/Tests/ConnectionPoolModuleTests/PoolStateMachine+RequestQueueTests.swift @@ -11,7 +11,7 @@ import Testing var queue = TestQueue() #expect(queue.isEmpty) - let request1 = MockRequest(connectionType: MockConnection.self) + let request1 = MockRequest(connectionType: MockConnection.self) queue.queue(request1) #expect(queue.count == 1) #expect(!queue.isEmpty) @@ -26,11 +26,11 @@ import Testing var queue = TestQueue() #expect(queue.isEmpty) - var request1 = MockRequest(connectionType: MockConnection.self) + var request1 = MockRequest(connectionType: MockConnection.self) queue.queue(request1) - var request2 = MockRequest(connectionType: MockConnection.self) + var request2 = MockRequest(connectionType: MockConnection.self) queue.queue(request2) - var request3 = MockRequest(connectionType: MockConnection.self) + var request3 = MockRequest(connectionType: MockConnection.self) queue.queue(request3) do { @@ -51,11 +51,11 @@ import Testing var queue = TestQueue() #expect(queue.isEmpty) - var request1 = MockRequest(connectionType: MockConnection.self) + var request1 = MockRequest(connectionType: MockConnection.self) queue.queue(request1) - var request2 = MockRequest(connectionType: MockConnection.self) + var request2 = MockRequest(connectionType: MockConnection.self) queue.queue(request2) - var request3 = MockRequest(connectionType: MockConnection.self) + var request3 = MockRequest(connectionType: MockConnection.self) queue.queue(request3) do { @@ -79,11 +79,11 @@ import Testing var queue = TestQueue() #expect(queue.isEmpty) - var request1 = MockRequest(connectionType: MockConnection.self) + var request1 = MockRequest(connectionType: MockConnection.self) queue.queue(request1) - var request2 = MockRequest(connectionType: MockConnection.self) + var request2 = MockRequest(connectionType: MockConnection.self) queue.queue(request2) - var request3 = MockRequest(connectionType: MockConnection.self) + var request3 = MockRequest(connectionType: MockConnection.self) queue.queue(request3) do { @@ -117,11 +117,11 @@ import Testing var queue = TestQueue() #expect(queue.isEmpty) - var request1 = MockRequest(connectionType: MockConnection.self) + var request1 = MockRequest(connectionType: MockConnection.self) queue.queue(request1) - var request2 = MockRequest(connectionType: MockConnection.self) + var request2 = MockRequest(connectionType: MockConnection.self) queue.queue(request2) - var request3 = MockRequest(connectionType: MockConnection.self) + var request3 = MockRequest(connectionType: MockConnection.self) queue.queue(request3) do { diff --git a/Tests/ConnectionPoolModuleTests/PoolStateMachineTests.swift b/Tests/ConnectionPoolModuleTests/PoolStateMachineTests.swift index c748de28..91f13f5b 100644 --- a/Tests/ConnectionPoolModuleTests/PoolStateMachineTests.swift +++ b/Tests/ConnectionPoolModuleTests/PoolStateMachineTests.swift @@ -4,15 +4,17 @@ import Testing @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) typealias TestPoolStateMachine = PoolStateMachine< - MockConnection, + MockConnection, ConnectionIDGenerator, MockConnection.ID, - MockRequest, - MockRequest.ID, + MockRequest>, + MockRequest>.ID, MockTimerCancellationToken > @Suite struct PoolStateMachineTests { + let executor = NothingConnectionPoolExecutor() + @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) @Test func testConnectionsAreCreatedAndParkedOnStartup() { @@ -28,8 +30,8 @@ typealias TestPoolStateMachine = PoolStateMachine< timerCancellationTokenType: MockTimerCancellationToken.self ) - let connection1 = MockConnection(id: 0) - let connection2 = MockConnection(id: 1) + let connection1 = MockConnection(id: 0, executor: self.executor) + let connection2 = MockConnection(id: 1, executor: self.executor) do { let requests = stateMachine.refillConnections() @@ -66,7 +68,7 @@ typealias TestPoolStateMachine = PoolStateMachine< timerCancellationTokenType: MockTimerCancellationToken.self ) - let connection1 = MockConnection(id: 0) + let connection1 = MockConnection(id: 0, executor: self.executor) // refill pool to at least one connection let requests = stateMachine.refillConnections() @@ -76,7 +78,7 @@ typealias TestPoolStateMachine = PoolStateMachine< #expect(createdAction1.connection == .scheduleTimers([])) // lease connection 1 - let request1 = MockRequest(connectionType: MockConnection.self) + let request1 = MockRequest(connectionType: MockConnection.self) let leaseRequest1 = stateMachine.leaseConnection(request1) #expect(leaseRequest1.connection == .cancelTimers([])) #expect(leaseRequest1.request == .leaseConnection(.init(element: request1), connection1)) @@ -85,19 +87,19 @@ typealias TestPoolStateMachine = PoolStateMachine< #expect(stateMachine.releaseConnection(connection1, streams: 1) == .none()) // lease connection 1 - let request2 = MockRequest(connectionType: MockConnection.self) + let request2 = MockRequest(connectionType: MockConnection.self) let leaseRequest2 = stateMachine.leaseConnection(request2) #expect(leaseRequest2.connection == .cancelTimers([])) #expect(leaseRequest2.request == .leaseConnection(.init(element: request2), connection1)) // request connection while none is available - let request3 = MockRequest(connectionType: MockConnection.self) + let request3 = MockRequest(connectionType: MockConnection.self) let leaseRequest3 = stateMachine.leaseConnection(request3) #expect(leaseRequest3.connection == .makeConnection(.init(connectionID: 1), [])) #expect(leaseRequest3.request == .none) // make connection 2 and lease immediately - let connection2 = MockConnection(id: 1) + let connection2 = MockConnection(id: 1, executor: NothingConnectionPoolExecutor()) let createdAction2 = stateMachine.connectionEstablished(connection2, maxStreams: 1) #expect(createdAction2.request == .leaseConnection(.init(element: request3), connection2)) #expect(createdAction2.connection == .none) @@ -134,19 +136,19 @@ typealias TestPoolStateMachine = PoolStateMachine< #expect(requests.count == 0) // request connection while none exists - let request1 = MockRequest(connectionType: MockConnection.self) + let request1 = MockRequest(connectionType: MockConnection.self) let leaseRequest1 = stateMachine.leaseConnection(request1) #expect(leaseRequest1.connection == .makeConnection(.init(connectionID: 0), [])) #expect(leaseRequest1.request == .none) // make connection 1 and lease immediately - let connection1 = MockConnection(id: 0) + let connection1 = MockConnection(id: 0, executor: self.executor) let createdAction1 = stateMachine.connectionEstablished(connection1, maxStreams: 1) #expect(createdAction1.request == .leaseConnection(.init(element: request1), connection1)) #expect(createdAction1.connection == .none) // request connection while none is available - let request2 = MockRequest(connectionType: MockConnection.self) + let request2 = MockRequest(connectionType: MockConnection.self) let leaseRequest2 = stateMachine.leaseConnection(request2) #expect(leaseRequest2.connection == .makeConnection(.init(connectionID: 1), [])) #expect(leaseRequest2.request == .none) @@ -157,7 +159,7 @@ typealias TestPoolStateMachine = PoolStateMachine< #expect(releaseRequest1.connection == .none) // connection 2 comes up and should be closed right away - let connection2 = MockConnection(id: 1) + let connection2 = MockConnection(id: 1, executor: self.executor) let createdAction2 = stateMachine.connectionEstablished(connection2, maxStreams: 1) #expect(createdAction2.request == .none) #expect(createdAction2.connection == .closeConnection(connection2, [])) @@ -188,7 +190,7 @@ typealias TestPoolStateMachine = PoolStateMachine< timerCancellationTokenType: MockTimerCancellationToken.self ) - let connection1 = MockConnection(id: 0) + let connection1 = MockConnection(id: 0, executor: self.executor) // refill pool to at least one connection let requests = stateMachine.refillConnections() @@ -198,19 +200,19 @@ typealias TestPoolStateMachine = PoolStateMachine< #expect(createdAction1.connection == .scheduleTimers([])) // lease connection 1 - let request1 = MockRequest(connectionType: MockConnection.self) + let request1 = MockRequest(connectionType: MockConnection.self) let leaseRequest1 = stateMachine.leaseConnection(request1) #expect(leaseRequest1.connection == .cancelTimers([])) #expect(leaseRequest1.request == .leaseConnection(.init(element: request1), connection1)) // request connection while none is available - let request2 = MockRequest(connectionType: MockConnection.self) + let request2 = MockRequest(connectionType: MockConnection.self) let leaseRequest2 = stateMachine.leaseConnection(request2) #expect(leaseRequest2.connection == .makeConnection(.init(connectionID: 1), [])) #expect(leaseRequest2.request == .none) // make connection 2 and lease immediately - let connection2 = MockConnection(id: 1) + let connection2 = MockConnection(id: 1, executor: self.executor) let createdAction2 = stateMachine.connectionEstablished(connection2, maxStreams: 1) #expect(createdAction2.request == .leaseConnection(.init(element: request2), connection2)) #expect(createdAction2.connection == .none) @@ -249,13 +251,13 @@ typealias TestPoolStateMachine = PoolStateMachine< #expect(requests.count == 0) // request connection while none exists - let request1 = MockRequest(connectionType: MockConnection.self) + let request1 = MockRequest(connectionType: MockConnection.self) let leaseRequest1 = stateMachine.leaseConnection(request1) #expect(leaseRequest1.connection == .makeConnection(.init(connectionID: 0), [])) #expect(leaseRequest1.request == .none) // make connection 1 and lease immediately - let connection1 = MockConnection(id: 0) + let connection1 = MockConnection(id: 0, executor: self.executor) let createdAction1 = stateMachine.connectionEstablished(connection1, maxStreams: 1) #expect(createdAction1.request == .leaseConnection(.init(element: request1), connection1)) #expect(createdAction1.connection == .none) @@ -291,13 +293,13 @@ typealias TestPoolStateMachine = PoolStateMachine< #expect(requests.count == 0) // request connection while none exists - let request1 = MockRequest(connectionType: MockConnection.self) + let request1 = MockRequest(connectionType: MockConnection.self) let leaseRequest1 = stateMachine.leaseConnection(request1) #expect(leaseRequest1.connection == .makeConnection(.init(connectionID: 0), [])) #expect(leaseRequest1.request == .none) // make connection 1 - let connection1 = MockConnection(id: 0) + let connection1 = MockConnection(id: 0, executor: self.executor) let createdAction1 = stateMachine.connectionEstablished(connection1, maxStreams: 1) #expect(createdAction1.request == .leaseConnection(.init(element: request1), connection1)) #expect(createdAction1.connection == .none) @@ -313,13 +315,13 @@ typealias TestPoolStateMachine = PoolStateMachine< connection1.closeIfClosing() // request connection while none exists anymore - let request2 = MockRequest(connectionType: MockConnection.self) + let request2 = MockRequest(connectionType: MockConnection.self) let leaseRequest2 = stateMachine.leaseConnection(request2) #expect(leaseRequest2.connection == .makeConnection(.init(connectionID: 1), [])) #expect(leaseRequest2.request == .none) // make connection 2 - let connection2 = MockConnection(id: 1) + let connection2 = MockConnection(id: 1, executor: self.executor) let createdAction2 = stateMachine.connectionEstablished(connection2, maxStreams: 1) #expect(createdAction2.request == .leaseConnection(.init(element: request2), connection2)) #expect(createdAction2.connection == .none) @@ -359,13 +361,13 @@ typealias TestPoolStateMachine = PoolStateMachine< #expect(requests.count == 1) // one connection should exist - let request = MockRequest(connectionType: MockConnection.self) + let request = MockRequest(connectionType: MockConnection.self) let leaseRequest = stateMachine.leaseConnection(request) #expect(leaseRequest.connection == .none) #expect(leaseRequest.request == .none) // make connection 1 - let connection = MockConnection(id: 0) + let connection = MockConnection(id: 0, executor: self.executor) let createdAction = stateMachine.connectionEstablished(connection, maxStreams: 1) #expect(createdAction.request == .leaseConnection(.init(element: request), connection)) #expect(createdAction.connection == .none) @@ -381,7 +383,7 @@ typealias TestPoolStateMachine = PoolStateMachine< let connectionClosed = stateMachine.connectionClosed(connection) #expect(connectionClosed.connection == .makeConnection(.init(connectionID: 1), [])) connection.closeIfClosing() - let establishAction = stateMachine.connectionEstablished(.init(id: 1), maxStreams: 1) + let establishAction = stateMachine.connectionEstablished(.init(id: 1, executor: self.executor), maxStreams: 1) #expect(establishAction.request == .none) if case .scheduleTimers(let timers) = establishAction.connection { #expect(timers == [.init(.init(timerID: 0, connectionID: 1, usecase: .keepAlive), duration: configuration.keepAliveDuration!)]) @@ -389,5 +391,4 @@ typealias TestPoolStateMachine = PoolStateMachine< Issue.record("Unexpected connection action") } } - } diff --git a/docker-compose.yml b/docker-compose.yml index 3eff4249..ee66a12d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ version: '3.7' x-shared-config: &shared_config + command: -c 'max_connections=500' environment: POSTGRES_HOST_AUTH_METHOD: "${POSTGRES_HOST_AUTH_METHOD:-scram-sha-256}" POSTGRES_USER: test_username