├── .github ├── CODEOWNERS ├── workflows │ ├── api-docs.yml │ └── test.yml └── dependabot.yml ├── .gitignore ├── .spi.yml ├── Sources └── AsyncKit │ ├── ConnectionPool │ ├── ConnectionPoolItem.swift │ ├── ConnectionPoolError.swift │ ├── ConnectionPoolSource.swift │ ├── EventLoopGroupConnectionPool.swift │ └── EventLoopConnectionPool.swift │ ├── Exports.swift │ ├── EventLoopFuture │ ├── Future+Transform.swift │ ├── Future+Try.swift │ ├── Future+Miscellaneous.swift │ ├── EventLoopFutureQueue+Sequence.swift │ ├── Collection+Flatten.swift │ ├── Future+Optional.swift │ ├── Future+Nonempty.swift │ ├── EventLoopFutureQueue.swift │ ├── FutureOperators.swift │ ├── Optional+StrictMap.swift │ └── Future+Collection.swift │ ├── EventLoop │ ├── EventLoop+Concurrency.swift │ ├── EventLoopGroup+Throwing.swift │ ├── EventLoopGroup+Concurrency.swift │ ├── EventLoop+Flatten.swift │ └── EventLoop+Future.swift │ └── Docs.docc │ ├── theme-settings.json │ ├── Resources │ └── vapor-asynckit-logo.svg │ └── index.md ├── Tests └── AsyncKitTests │ ├── EventLoop+ConcurrencyTests.swift │ ├── Future+TransformTests.swift │ ├── EventLoopGroup+FutureTests.swift │ ├── Future+MiscellaneousTests.swift │ ├── Future+TryTests.swift │ ├── AsyncKitTestsCommon.swift │ ├── Collection+FlattenTests.swift │ ├── Future+NonemptyTests.swift │ ├── Future+OptionalTests.swift │ ├── FutureOperatorsTests.swift │ ├── AsyncConnectionPoolTests.swift │ ├── EventLoopFutureQueueTests.swift │ ├── Future+CollectionTests.swift │ ├── Future+ConjunctionTests.swift │ └── ConnectionPoolTests.swift ├── LICENSE ├── README.md └── Package.swift /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @0xTim @gwynne 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | DerivedData 6 | Package.resolved 7 | .swiftpm 8 | 9 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | metadata: 3 | authors: "Maintained by the Vapor Core Team with hundreds of contributions from the Vapor Community." 4 | external_links: 5 | documentation: "https://api.vapor.codes/asynckit/documentation/asynckit/" 6 | -------------------------------------------------------------------------------- /.github/workflows/api-docs.yml: -------------------------------------------------------------------------------- 1 | name: deploy-api-docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-and-deploy: 9 | uses: vapor/api-docs/.github/workflows/build-and-deploy-docs-workflow.yml@main 10 | secrets: inherit 11 | with: 12 | package_name: async-kit 13 | modules: AsyncKit 14 | pathsToInvalidate: /asynckit/* -------------------------------------------------------------------------------- /Sources/AsyncKit/ConnectionPool/ConnectionPoolItem.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | /// Item managed by a connection pool. 4 | public protocol ConnectionPoolItem: AnyObject { 5 | /// `EventLoop` this connection belongs to. 6 | var eventLoop: any EventLoop { get } 7 | 8 | /// If `true`, this connection has closed. 9 | var isClosed: Bool { get } 10 | 11 | /// Closes this connection. 12 | func close() -> EventLoopFuture 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.ref }} 4 | cancel-in-progress: true 5 | on: 6 | pull_request: { types: [opened, reopened, synchronize, ready_for_review] } 7 | push: { branches: [ main ] } 8 | 9 | env: 10 | LOG_LEVEL: info 11 | SWIFT_DETERMINISTIC_HASHING: 1 12 | 13 | jobs: 14 | unit-tests: 15 | uses: vapor/ci/.github/workflows/run-unit-tests.yml@main 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "swift" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | groups: 9 | dependencies: 10 | patterns: 11 | - "*" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | groups: 17 | dependencies: 18 | patterns: 19 | - "*" 20 | -------------------------------------------------------------------------------- /Sources/AsyncKit/ConnectionPool/ConnectionPoolError.swift: -------------------------------------------------------------------------------- 1 | /// Errors thrown by ``EventLoopGroupConnectionPool`` and ``EventLoopConnectionPool`` regarding state. 2 | public enum ConnectionPoolError: Error { 3 | /// The connection pool has shutdown. 4 | case shutdown 5 | } 6 | 7 | /// Errors thrown by ``EventLoopGroupConnectionPool`` and ``EventLoopConnectionPool`` regarding timeouts. 8 | public enum ConnectionPoolTimeoutError: Error { 9 | /// The connection request timed out. 10 | case connectionRequestTimeout 11 | } 12 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/EventLoop+ConcurrencyTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import AsyncKit 3 | import NIOCore 4 | 5 | final class EventLoopConcurrencyTests: AsyncKitTestCase { 6 | func testGroupMakeFutureWithTask() throws { 7 | @Sendable 8 | func getOne() async throws -> Int { 9 | return 1 10 | } 11 | let expectedOne = self.group.makeFutureWithTask { 12 | try await getOne() 13 | } 14 | 15 | XCTAssertEqual(try expectedOne.wait(), 1) 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Sources/AsyncKit/ConnectionPool/ConnectionPoolSource.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import struct Logging.Logger 3 | 4 | /// Source of new connections for ``EventLoopGroupConnectionPool``. 5 | public protocol ConnectionPoolSource { 6 | /// Associated ``ConnectionPoolItem`` that will be returned by ``ConnectionPoolSource/makeConnection(logger:on:)``. 7 | associatedtype Connection: ConnectionPoolItem 8 | 9 | /// Creates a new connection. 10 | func makeConnection(logger: Logger, on eventLoop: any EventLoop) -> EventLoopFuture 11 | } 12 | -------------------------------------------------------------------------------- /Sources/AsyncKit/Exports.swift: -------------------------------------------------------------------------------- 1 | @_documentation(visibility: internal) @_exported import class NIOEmbedded.EmbeddedEventLoop 2 | @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop 3 | @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoopGroup 4 | @_documentation(visibility: internal) @_exported import class NIOCore.EventLoopFuture 5 | @_documentation(visibility: internal) @_exported import struct NIOCore.EventLoopPromise 6 | @_documentation(visibility: internal) @_exported import class NIOPosix.MultiThreadedEventLoopGroup 7 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/Future+TransformTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import AsyncKit 3 | import NIOCore 4 | 5 | class FutureTransformTests: AsyncKitTestCase { 6 | func testTransforms() throws { 7 | let future = eventLoop.makeSucceededFuture(Int.random(in: 0...100)) 8 | 9 | XCTAssert(try future.transform(to: true).wait()) 10 | 11 | let futureA = eventLoop.makeSucceededFuture(Int.random(in: 0...100)) 12 | let futureB = eventLoop.makeSucceededFuture(Int.random(in: 0...100)) 13 | 14 | XCTAssert(try futureA.and(futureB).transform(to: true).wait()) 15 | 16 | let futureBool = eventLoop.makeSucceededFuture(true) 17 | 18 | XCTAssert(try future.transform(to: futureBool).wait()) 19 | 20 | XCTAssert(try futureA.and(futureB).transform(to: futureBool).wait()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/Future+Transform.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopFuture { 4 | /// Maps the current future to contain the new type. Errors are carried over, successful (expected) results are transformed into the given instance. 5 | /// 6 | /// user.save(on: req).transform(to: HTTPStatus.created) 7 | /// 8 | public func transform(to instance: @escaping @autoclosure () -> T) -> EventLoopFuture { 9 | return self.map { _ in 10 | instance() 11 | } 12 | } 13 | 14 | /// Maps the current future to contain the new type. Errors are carried over, successful (expected) results are transformed into the given instance. 15 | /// 16 | /// let user = User.find(id, on: request) 17 | /// posts.save(on: request).transform(to: user) 18 | /// 19 | public func transform(to future: EventLoopFuture) -> EventLoopFuture { 20 | return self.flatMap { _ in 21 | future 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoop/EventLoop+Concurrency.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoop { 4 | /// Run the `async` function `body` on this event loop and return its result as 5 | /// an `EventLoopFuture`. 6 | /// 7 | /// This function can be used to bridge the `async` world into an `EventLoop`. 8 | /// 9 | /// This method is deprecated. Call `EventLoop.makeFutureWithTask(_:)` directly 10 | /// instead. 11 | /// 12 | /// - parameters: 13 | /// - body: The `async` function to run. 14 | /// - returns: An `EventLoopFuture` which is completed when `body` finishes. On 15 | /// success the future has the result returned by `body`; if `body` throws an 16 | /// error, the future is failed with that error. 17 | @available(*, deprecated, renamed: "makeFutureWithTask(_:)") 18 | @inlinable 19 | public func performWithTask( 20 | _ body: @escaping @Sendable () async throws -> Value 21 | ) -> EventLoopFuture { 22 | return self.makeFutureWithTask(body) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/EventLoopGroup+FutureTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncKit 2 | import XCTest 3 | import NIOCore 4 | 5 | final class EventLoopGroupFutureTests: AsyncKitTestCase { 6 | func testFutureVoid() throws { 7 | try XCTAssertNoThrow(self.group.future().wait()) 8 | try XCTAssertNoThrow(self.eventLoop.future().wait()) 9 | } 10 | 11 | func testFuture() throws { 12 | try XCTAssertEqual(self.group.future(1).wait(), 1) 13 | try XCTAssertEqual(self.eventLoop.future(1).wait(), 1) 14 | 15 | try XCTAssertEqual(self.group.future(true).wait(), true) 16 | try XCTAssertEqual(self.eventLoop.future("foo").wait(), "foo") 17 | } 18 | 19 | func testFutureError() throws { 20 | let groupErr: EventLoopFuture = self.group.future(error: TestError.notEqualTo1) 21 | let eventLoopErr: EventLoopFuture = self.eventLoop.future(error: TestError.notEqualToBar) 22 | 23 | try XCTAssertThrowsError(groupErr.wait()) 24 | try XCTAssertThrowsError(eventLoopErr.wait()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vapor Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/AsyncKit/Docs.docc/theme-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "aside": { "border-radius": "6px", "border-style": "double", "border-width": "3px" }, 4 | "border-radius": "0", 5 | "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 6 | "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 7 | "color": { 8 | "asynckit": "#808080", 9 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-asynckit) 30%, #000 100%)", 10 | "documentation-intro-accent": "var(--color-asynckit)", 11 | "documentation-intro-eyebrow": "white", 12 | "documentation-intro-figure": "white", 13 | "documentation-intro-title": "white", 14 | "logo-base": { "dark": "#fff", "light": "#000" }, 15 | "logo-shape": { "dark": "#000", "light": "#fff" }, 16 | "fill": { "dark": "#000", "light": "#fff" } 17 | }, 18 | "icons": { "technology": "/asynckit/images/AsyncKit/vapor-asynckit-logo.svg" } 19 | }, 20 | "features": { 21 | "quickNavigation": { "enable": true }, 22 | "i18n": { "enable": true } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoop/EventLoopGroup+Throwing.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopGroup { 4 | /// An alternate name for this would be `future(catching:)`, but with that 5 | /// name, trailing closure syntax just looks like `el.future { ... }`, which 6 | /// does not indicate to readers of the code that it is the error-capturing 7 | /// version. Since such an indication is highly desirable, a slightly less 8 | /// idiomatic name is used instead. 9 | /// 10 | /// This method replaces this code: 11 | /// 12 | /// ```swift 13 | /// return something.eventLoop.future().flatMapThrowing { 14 | /// ``` 15 | /// 16 | /// With this code: 17 | /// 18 | /// ```swift 19 | /// return something.eventLoop.tryFuture { 20 | /// ``` 21 | /// 22 | /// That's pretty much it. It's sugar. 23 | /// 24 | /// - Parameter work: The potentially throwing closure to execute as a 25 | /// future. If the closure throws, a failed future is returned. 26 | public func tryFuture(_ work: @escaping () throws -> T) -> EventLoopFuture { 27 | return self.any().submit(work) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/AsyncKit/Docs.docc/Resources/vapor-asynckit-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoop/EventLoopGroup+Concurrency.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopGroup { 4 | /// Run the `async` function `body` on an event loop in this group and return its 5 | /// result as an `EventLoopFuture`. 6 | /// 7 | /// This function can be used to bridge the `async` world into an `EventLoopGroup`. 8 | /// 9 | /// See also `EventLoop.performWithTask(_:)`, `EventLoopPromise.completeWithTask(_:)` 10 | /// 11 | /// - parameters: 12 | /// - body: The `async` function to run. 13 | /// - returns: An `EventLoopFuture` which is completed when `body` finishes. On 14 | /// success the future has the result returned by `body`; if `body` throws an 15 | /// error, the future is failed with that error. 16 | @available(*, deprecated, renamed: "makeFutureWithTask(_:)") 17 | @inlinable 18 | public func performWithTask( 19 | _ body: @escaping @Sendable () async throws -> Value 20 | ) -> EventLoopFuture { 21 | return self.makeFutureWithTask(body) 22 | } 23 | 24 | @inlinable 25 | public func makeFutureWithTask( 26 | _ body: @escaping @Sendable () async throws -> Value 27 | ) -> EventLoopFuture { 28 | return self.any().makeFutureWithTask(body) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/Future+MiscellaneousTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import AsyncKit 3 | import NIOCore 4 | 5 | final class FutureMiscellaneousTests: AsyncKitTestCase { 6 | func testGuard() { 7 | let future1 = eventLoop.makeSucceededFuture(1) 8 | let guardedFuture1 = future1.guard({ $0 == 1 }, else: TestError.notEqualTo1) 9 | XCTAssertNoThrow(try guardedFuture1.wait()) 10 | 11 | let future2 = eventLoop.makeSucceededFuture("foo") 12 | let guardedFuture2 = future2.guard({ $0 == "bar" }, else: TestError.notEqualToBar) 13 | XCTAssertThrowsError(try guardedFuture2.wait()) 14 | } 15 | 16 | func testTryThrowing() { 17 | let future1: EventLoopFuture = eventLoop.tryFuture { return "Hello" } 18 | let future2: EventLoopFuture = eventLoop.tryFuture { throw TestError.notEqualTo1 } 19 | var value: String = "" 20 | 21 | try XCTAssertNoThrow(value = future1.wait()) 22 | XCTAssertEqual(value, "Hello") 23 | try XCTAssertThrowsError(future2.wait()) 24 | } 25 | 26 | func testTryFutureThread() throws { 27 | let future = self.eventLoop.tryFuture { Thread.current.name } 28 | let name = try XCTUnwrap(future.wait()) 29 | 30 | XCTAssert(name.starts(with: "NIO-ELT"), "'\(name)' is not a valid NIO ELT name") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/Future+Try.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopFuture { 4 | public func tryFlatMap( 5 | file _: StaticString = #file, line _: UInt = #line, 6 | _ callback: @escaping (Value) throws -> EventLoopFuture 7 | ) -> EventLoopFuture { 8 | /// When the current `EventLoopFuture` is fulfilled, run the provided callback, 9 | /// which will provide a new `EventLoopFuture`. 10 | /// 11 | /// This allows you to dynamically dispatch new asynchronous tasks as phases in a 12 | /// longer series of processing steps. Note that you can use the results of the 13 | /// current `EventLoopFuture` when determining how to dispatch the next operation. 14 | /// 15 | /// The key difference between this method and the regular `flatMap` is error handling. 16 | /// 17 | /// With `tryFlatMap`, the provided callback _may_ throw Errors, causing the returned `EventLoopFuture` 18 | /// to report failure immediately after the completion of the original `EventLoopFuture`. 19 | return self.flatMap() { [eventLoop] value in 20 | do { 21 | return try callback(value) 22 | } catch { 23 | return eventLoop.makeFailedFuture(error) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoop/EventLoop+Flatten.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoop { 4 | /// Returns a new `EventLoopFuture` that succeeds only when all the provided futures succeed. 5 | /// The new `EventLoopFuture` contains an array of results, maintaining same ordering as the futures. 6 | /// 7 | /// The returned `EventLoopFuture` will fail if any of the provided futures fails. All remaining 8 | /// `EventLoopFuture` objects will be ignored. 9 | /// - Parameter futures: An array of futures to flatten into a single `EventLoopFuture`. 10 | /// - Returns: A new `EventLoopFuture` with all the resolved values of the input collection. 11 | public func flatten(_ futures: [EventLoopFuture]) -> EventLoopFuture<[T]> { 12 | return EventLoopFuture.whenAllSucceed(futures, on: self) 13 | } 14 | 15 | /// Returns a new `EventLoopFuture` that succeeds only when all the provided futures succeed, 16 | /// ignoring the resolved values. 17 | /// 18 | /// The returned `EventLoopFuture` will fail if any of the provided futures fails. All remaining 19 | /// `EventLoopFuture` objects will be ignored. 20 | /// - Parameter futures: An array of futures to wait for. 21 | /// - Returns: A new `EventLoopFuture`. 22 | public func flatten(_ futures: [EventLoopFuture]) -> EventLoopFuture { 23 | return EventLoopFuture.whenAllSucceed(futures, on: self).map { _ in () } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/Future+TryTests.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import AsyncKit 3 | import XCTest 4 | 5 | final class FutureTryTests: AsyncKitTestCase { 6 | func testTryFlatMapPropagatesCallbackError() { 7 | let future = eventLoop.future(0) 8 | .tryFlatMap { _ -> EventLoopFuture in 9 | throw TestError.generic 10 | } 11 | 12 | XCTAssertThrowsError(try future.wait()) { error in 13 | guard case TestError.generic = error else { 14 | XCTFail("Received an unexpected error: \(error)") 15 | return 16 | } 17 | } 18 | } 19 | 20 | func testTryFlatMapPropagatesInnerError() { 21 | let future = eventLoop.future(0) 22 | .tryFlatMap { _ -> EventLoopFuture in 23 | self.eventLoop.makeFailedFuture(TestError.generic) 24 | } 25 | 26 | XCTAssertThrowsError(try future.wait()) { error in 27 | guard case TestError.generic = error else { 28 | XCTFail("Received an unexpected error: \(error)") 29 | return 30 | } 31 | } 32 | } 33 | 34 | func testTryFlatMapPropagatesResult() throws { 35 | let future = eventLoop.future(0) 36 | .tryFlatMap { value -> EventLoopFuture in 37 | self.eventLoop.makeSucceededFuture(String(describing: value)) 38 | } 39 | 40 | try XCTAssertEqual("0", future.wait()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | AsyncKit 6 | 7 |
8 |
9 | Documentation 10 | Team Chat 11 | MIT License 12 | Continuous Integration 13 | 14 | Swift 5.10+ 15 |

16 | 17 |
18 | 19 | _**AsyncKit is a legacy package; its use is not recommended in new projects.**_ 20 | 21 | This package provides a set of utilities for working with [EventLoopFuture](https://swiftpackageindex.com/apple/swift-nio/main/documentation/niocore/eventloopfuture)s and other related pre-Concurrency support APIs. 22 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/AsyncKitTestsCommon.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import AsyncKit 3 | import NIOCore 4 | import NIOPosix 5 | import Logging 6 | 7 | enum TestError: Error { 8 | case generic 9 | case notEqualTo1 10 | case notEqualToBar 11 | } 12 | 13 | func env(_ name: String) -> String? { 14 | return ProcessInfo.processInfo.environment[name] 15 | } 16 | 17 | let isLoggingConfigured: Bool = { 18 | LoggingSystem.bootstrap { label in 19 | var handler = StreamLogHandler.standardOutput(label: label) 20 | handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .info 21 | return handler 22 | } 23 | return true 24 | }() 25 | 26 | class AsyncKitTestCase: XCTestCase { 27 | var group: (any EventLoopGroup)! 28 | var eventLoop: any EventLoop { self.group.any() } 29 | 30 | override func setUpWithError() throws { 31 | try super.setUpWithError() 32 | self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 33 | } 34 | 35 | override func tearDownWithError() throws { 36 | try self.group.syncShutdownGracefully() 37 | self.group = nil 38 | try super.tearDownWithError() 39 | } 40 | 41 | override class func setUp() { 42 | super.setUp() 43 | XCTAssertTrue(isLoggingConfigured) 44 | } 45 | } 46 | 47 | class AsyncKitAsyncTestCase: XCTestCase { 48 | var group: (any EventLoopGroup)! 49 | var eventLoop: any EventLoop { self.group.any() } 50 | 51 | override func setUp() async throws { 52 | try await super.setUp() 53 | self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 54 | XCTAssertTrue(isLoggingConfigured) 55 | } 56 | 57 | override func tearDown() async throws { 58 | try await self.group.shutdownGracefully() 59 | self.group = nil 60 | try await super.tearDown() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/Collection+FlattenTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncKit 2 | import XCTest 3 | import NIOCore 4 | 5 | final class CollectionFlattenTests: AsyncKitTestCase { 6 | func testELFlatten()throws { 7 | let futures = [ 8 | self.eventLoop.makeSucceededFuture(1), 9 | self.eventLoop.makeSucceededFuture(2), 10 | self.eventLoop.makeSucceededFuture(3), 11 | self.eventLoop.makeSucceededFuture(4), 12 | self.eventLoop.makeSucceededFuture(5), 13 | self.eventLoop.makeSucceededFuture(6), 14 | self.eventLoop.makeSucceededFuture(7) 15 | ] 16 | 17 | let flattened = self.eventLoop.flatten(futures) 18 | try XCTAssertEqual(flattened.wait(), [1, 2, 3, 4, 5, 6, 7]) 19 | 20 | let voids = [ 21 | self.eventLoop.makeSucceededFuture(()), 22 | self.eventLoop.makeSucceededFuture(()), 23 | self.eventLoop.makeSucceededFuture(()), 24 | self.eventLoop.makeSucceededFuture(()), 25 | self.eventLoop.makeSucceededFuture(()), 26 | self.eventLoop.makeSucceededFuture(()), 27 | self.eventLoop.makeSucceededFuture(()) 28 | ] 29 | 30 | let void = self.eventLoop.flatten(voids) 31 | try XCTAssert(void.wait() == ()) 32 | } 33 | 34 | func testCollectionFlatten()throws { 35 | let futures = [ 36 | self.eventLoop.makeSucceededFuture(1), 37 | self.eventLoop.makeSucceededFuture(2), 38 | self.eventLoop.makeSucceededFuture(3), 39 | self.eventLoop.makeSucceededFuture(4), 40 | self.eventLoop.makeSucceededFuture(5), 41 | self.eventLoop.makeSucceededFuture(6), 42 | self.eventLoop.makeSucceededFuture(7) 43 | ] 44 | 45 | let flattened = futures.flatten(on: self.eventLoop) 46 | try XCTAssertEqual(flattened.wait(), [1, 2, 3, 4, 5, 6, 7]) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoop/EventLoop+Future.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopGroup { 4 | /// Creates a new, succeeded `EventLoopFuture` from the worker's event loop with a `Void` value. 5 | /// 6 | /// let a: EventLoopFuture = req.future() 7 | /// 8 | /// - Returns: The succeeded future. 9 | public func future() -> EventLoopFuture { 10 | return self.any().makeSucceededFuture(()) 11 | } 12 | 13 | /// Creates a new, succeeded `EventLoopFuture` from the worker's event loop. 14 | /// 15 | /// let a: EventLoopFuture = req.future("hello") 16 | /// 17 | /// - Parameter value: The value that the future will wrap. 18 | /// - Returns: The succeeded future. 19 | public func future(_ value: T) -> EventLoopFuture { 20 | return self.any().makeSucceededFuture(value) 21 | } 22 | 23 | /// Creates a new, failed `EventLoopFuture` from the worker's event loop. 24 | /// 25 | /// let b: EvenLoopFuture = req.future(error: Abort(...)) 26 | /// 27 | /// - Parameter error: The error that the future will wrap. 28 | /// - Returns: The failed future. 29 | public func future(error: any Error) -> EventLoopFuture { 30 | return self.any().makeFailedFuture(error) 31 | } 32 | 33 | /// Creates a new `Future` from the worker's event loop, succeeded or failed based on the input `Result`. 34 | /// 35 | /// let a: EventLoopFuture = req.future(.success("hello")) 36 | /// let b: EventLoopFuture = req.future(.failed(Abort(.imATeapot)) 37 | /// 38 | /// - Parameter result: The result that the future will wrap. 39 | /// - Returns: The succeeded or failed future. 40 | public func future(result: Result) -> EventLoopFuture { 41 | let promise: EventLoopPromise = self.any().makePromise() 42 | promise.completeWith(result) 43 | return promise.futureResult 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "async-kit", 6 | platforms: [ 7 | .macOS(.v10_15), 8 | .iOS(.v13), 9 | .watchOS(.v6), 10 | .tvOS(.v13), 11 | ], 12 | products: [ 13 | .library(name: "AsyncKit", targets: ["AsyncKit"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.61.0"), 17 | .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), 18 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.5"), 19 | .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.1.0"), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "AsyncKit", 24 | dependencies: [ 25 | .product(name: "Logging", package: "swift-log"), 26 | .product(name: "NIOCore", package: "swift-nio"), 27 | .product(name: "NIOEmbedded", package: "swift-nio"), 28 | .product(name: "NIOPosix", package: "swift-nio"), 29 | .product(name: "Collections", package: "swift-collections"), 30 | .product(name: "Algorithms", package: "swift-algorithms"), 31 | ], 32 | swiftSettings: swiftSettings 33 | ), 34 | .testTarget( 35 | name: "AsyncKitTests", 36 | dependencies: [ 37 | .target(name: "AsyncKit"), 38 | ], 39 | swiftSettings: swiftSettings 40 | ), 41 | ] 42 | ) 43 | 44 | var swiftSettings: [SwiftSetting] { [ 45 | .enableUpcomingFeature("ExistentialAny"), 46 | .enableUpcomingFeature("ConciseMagicFile"), 47 | .enableUpcomingFeature("ForwardTrailingClosures"), 48 | //.enableUpcomingFeature("DisableOutwardActorInference"), 49 | .enableUpcomingFeature("MemberImportVisibility"), 50 | //.enableExperimentalFeature("StrictConcurrency=complete"), 51 | ] } 52 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/Future+NonemptyTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import AsyncKit 3 | import NIOCore 4 | 5 | final class FutureNonemptyTests: AsyncKitTestCase { 6 | func testNonempty() { 7 | try XCTAssertNoThrow(self.eventLoop.future([0]).nonempty(orError: TestError.notEqualTo1).wait()) 8 | try XCTAssertThrowsError(self.eventLoop.future([]).nonempty(orError: TestError.notEqualTo1).wait()) 9 | 10 | XCTAssertEqual(try self.eventLoop.future([0]).nonemptyMap(or: 1, { $0[0] }).wait(), 0) 11 | XCTAssertEqual(try self.eventLoop.future([]).nonemptyMap(or: 1, { $0[0] }).wait(), 1) 12 | 13 | XCTAssertEqual(try self.eventLoop.future([0]).nonemptyMap({ [$0[0]] }).wait(), [0]) 14 | XCTAssertEqual(try self.eventLoop.future([Int]()).nonemptyMap({ [$0[0]] }).wait(), []) 15 | 16 | XCTAssertEqual(try self.eventLoop.future([0]).nonemptyFlatMapThrowing(or: 1, { (a) throws -> Int in a[0] }).wait(), 0) 17 | XCTAssertEqual(try self.eventLoop.future([]).nonemptyFlatMapThrowing(or: 1, { (a) throws -> Int in a[0] }).wait(), 1) 18 | 19 | XCTAssertEqual(try self.eventLoop.future([0]).nonemptyFlatMapThrowing({ (a) throws -> [Int] in [a[0]] }).wait(), [0]) 20 | XCTAssertEqual(try self.eventLoop.future([Int]()).nonemptyFlatMapThrowing({ (a) throws -> [Int] in [a[0]] }).wait(), []) 21 | 22 | XCTAssertEqual(try self.eventLoop.future([0]).nonemptyFlatMap(or: 1, { self.eventLoop.future($0[0]) }).wait(), 0) 23 | XCTAssertEqual(try self.eventLoop.future([]).nonemptyFlatMap(or: 1, { self.eventLoop.future($0[0]) }).wait(), 1) 24 | 25 | XCTAssertEqual(try self.eventLoop.future([0]).nonemptyFlatMap(orFlat: self.eventLoop.future(1), { self.eventLoop.future($0[0]) }).wait(), 0) 26 | XCTAssertEqual(try self.eventLoop.future([]).nonemptyFlatMap(orFlat: self.eventLoop.future(1), { self.eventLoop.future($0[0]) }).wait(), 1) 27 | 28 | XCTAssertEqual(try self.eventLoop.future([0]).nonemptyFlatMap({ self.eventLoop.future([$0[0]]) }).wait(), [0]) 29 | XCTAssertEqual(try self.eventLoop.future([Int]()).nonemptyFlatMap({ self.eventLoop.future([$0[0]]) }).wait(), []) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/Future+Miscellaneous.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopFuture { 4 | // MARK: - Guard 5 | 6 | /// Guards that the future's value satisfies the callback's condition or 7 | /// fails with the given error. 8 | /// 9 | /// 10 | /// Example usage: 11 | /// 12 | /// future.guard({ $0.userID == user.id }, else: AccessError.unauthorized) 13 | /// 14 | /// - parameters: 15 | /// - callback: Callback that asynchronously executes a condition. 16 | /// - error: The error to fail with if condition isn't met. 17 | /// - returns: A future containing the original future's result. 18 | public func `guard`(_ callback: @escaping ((Value) -> Bool), else error: @escaping @autoclosure () -> any Error) -> EventLoopFuture { 19 | let promise = self.eventLoop.makePromise(of: Value.self) 20 | self.whenComplete { result in 21 | switch result { 22 | case .success(let value): 23 | if callback(value) { 24 | promise.succeed(value) 25 | } else { 26 | promise.fail(error()) 27 | } 28 | case .failure(let error): promise.fail(error) 29 | } 30 | } 31 | return promise.futureResult 32 | } 33 | 34 | // MARK: - flatMapAlways 35 | 36 | /// When the current `EventLoopFuture` receives any result, run the provided callback, which will provide a new 37 | /// `EventLoopFuture`. Essentially combines the behaviors of `.always(_:)` and `.flatMap(file:line:_:)`. 38 | /// 39 | /// This is useful when some work must be done for both success and failure states, especially work that requires 40 | /// temporarily extending the lifetime of one or more objects. 41 | public func flatMapAlways( 42 | file: StaticString = #file, line: UInt = #line, 43 | _ callback: @escaping (Result) -> EventLoopFuture 44 | ) -> EventLoopFuture { 45 | let promise = self.eventLoop.makePromise(of: NewValue.self, file: file, line: line) 46 | self.whenComplete { result in callback(result).cascade(to: promise) } 47 | return promise.futureResult 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/Future+OptionalTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncKit 2 | import XCTest 3 | import NIOCore 4 | 5 | final class FutureOptionalTests: AsyncKitTestCase { 6 | func testOptionalMap() throws { 7 | let future = self.eventLoop.makeSucceededFuture(Optional.some(1)) 8 | let null = self.eventLoop.makeSucceededFuture(Optional.none) 9 | 10 | let times2 = future.optionalMap { $0 * 2 } 11 | let null2 = null.optionalMap { $0 * 2 } 12 | let nullResult = future.optionalMap { _ in Optional.none } 13 | 14 | try XCTAssertEqual(2, times2.wait()) 15 | try XCTAssertEqual(nil, null2.wait()) 16 | try XCTAssertEqual(nil, nullResult.wait()) 17 | } 18 | 19 | func testOptionalFlatMapThrowing() throws { 20 | let future = self.eventLoop.makeSucceededFuture(Optional.some(1)) 21 | 22 | let times2 = future.optionalFlatMapThrowing { $0 * 2 } 23 | let null2 = future.optionalFlatMapThrowing { return $0 % 2 == 0 ? $0 : nil } 24 | let error = future.optionalFlatMapThrowing { _ in throw TestError.generic } 25 | 26 | try XCTAssertEqual(2, times2.wait()) 27 | try XCTAssertEqual(nil, null2.wait()) 28 | try XCTAssertThrowsError(error.wait()) 29 | } 30 | 31 | func testOptionalFlatMap() throws { 32 | let future = self.eventLoop.makeSucceededFuture(Optional.some(1)) 33 | let null = self.eventLoop.makeSucceededFuture(Optional.none) 34 | 35 | var times2 = future.optionalFlatMap { self.multiply($0, 2) } 36 | var null2 = null.optionalFlatMap { self.multiply($0, 2) } 37 | 38 | try XCTAssertEqual(2, times2.wait()) 39 | try XCTAssertEqual(nil, null2.wait()) 40 | 41 | 42 | times2 = future.optionalFlatMap { self.multiply($0, Optional.some(2)) } 43 | null2 = future.optionalFlatMap { self.multiply($0, nil) } 44 | 45 | try XCTAssertEqual(2, times2.wait()) 46 | try XCTAssertEqual(nil, null2.wait()) 47 | } 48 | 49 | func multiply(_ a: Int, _ b: Int) -> EventLoopFuture { 50 | return self.group.any().makeSucceededFuture(a * b) 51 | } 52 | 53 | func multiply(_ a: Int, _ b: Int?) -> EventLoopFuture { 54 | return self.group.any().makeSucceededFuture(b == nil ? nil : a * b!) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/EventLoopFutureQueue+Sequence.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopFutureQueue { 4 | /// For each element of the provided collection, invoke the given generator 5 | /// and queue the returned future. Return a future whose value is an array 6 | /// containing the result of each generated future in the same order as the 7 | /// original sequence. The resulting array is intended to have semantics 8 | /// substantially similar to those provided by `EventLoop.flatten(_:on:)`. 9 | public func append(each seq: S, _ generator: @escaping (S.Element) -> EventLoopFuture) -> EventLoopFuture<[Value]> { 10 | // For each element in the sequence, obtain a generated future, add the result of that future to the result 11 | // array, map the future to `Void`, and append the result to this queue. Left with the final future in the 12 | // chain (representing the result of the final element in the sequence) via `reduce()`, append to the queue 13 | // a last future whose value is the results array, which should now be complete. The in-order and immediate- 14 | // halt-on-fail guarantees of the queue itself negate any need to maintain a separate `Promise` or use 15 | // `enumerated()` to ensure consistency of the results array (see `EventLoopFuture.whenAllComplete(_:on:)` for 16 | // more information). 17 | var count: Int = 0 // used for debugging assertions 18 | var results: [Value] = [] 19 | results.reserveCapacity(seq.underestimatedCount) 20 | 21 | seq.forEach { 22 | assert({ count += 1; return true }()) 23 | _ = self.append(generator($0).map { results.append($0) }, runningOn: .success) 24 | } 25 | return self.append(onPrevious: .success) { 26 | assert(results.count == count, "Sequence completed, but we didn't get all the results, or got too many - EventLoopFutureQueue is broken.") 27 | return self.eventLoop.future(results) 28 | } 29 | } 30 | 31 | /// Same as `append(each:_:)` above, but assumes all futures return `Void` 32 | /// and returns a `Void` future instead of a result array. 33 | public func append(each seq: S, _ generator: @escaping (S.Element) -> EventLoopFuture) -> EventLoopFuture { 34 | seq.forEach { _ = self.append(generator($0), runningOn: .success) } 35 | return self.append(self.eventLoop.future(), runningOn: .success) // returns correct future in case of empty sequence 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/Collection+Flatten.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension Collection { 4 | /// Converts a collection of `EventLoopFuture`s to an `EventLoopFuture` that wraps an array with the future values. 5 | /// 6 | /// Acts as a helper for the `EventLoop.flatten(_:[EventLoopFuture])` method. 7 | /// 8 | /// let futures = [el.future(1), el.future(2), el.future(3), el.future(4)] 9 | /// let flattened = futures.flatten(on: el) 10 | /// // flattened: EventLoopFuture([1, 2, 3, 4]) 11 | /// 12 | /// - parameter eventLoop: The event-loop to succeed the futures on. 13 | /// - returns: The succeeded values in an array, wrapped in an `EventLoopFuture`. 14 | public func flatten(on eventLoop: any EventLoop) -> EventLoopFuture<[Value]> 15 | where Element == EventLoopFuture 16 | { 17 | return eventLoop.flatten(Array(self)) 18 | } 19 | } 20 | 21 | extension Array where Element == EventLoopFuture { 22 | /// Converts a collection of `EventLoopFuture`s to an `EventLoopFuture`. 23 | /// 24 | /// Acts as a helper for the `EventLoop.flatten(_:[EventLoopFuture])` method. 25 | /// 26 | /// let futures = [el.future(1), el.future(2), el.future(3), el.future(4)] 27 | /// let flattened = futures.flatten(on: el) 28 | /// // flattened: EventLoopFuture 29 | /// 30 | /// - parameter eventLoop: The event-loop to succeed the futures on. 31 | /// - returns: The succeeded future. 32 | public func flatten(on eventLoop: any EventLoop) -> EventLoopFuture { 33 | return .andAllSucceed(self, on: eventLoop) 34 | } 35 | } 36 | 37 | extension Collection { 38 | /// Strictly sequentially transforms each element of the collection into an 39 | /// `EventLoopFuture`, collects the results of each future, and returns the 40 | /// overall result. Identical to `EventLoopFuture.sequencedFlatMapEach(_:)`, 41 | /// but does not require the initial collection to be wrapped by a future. 42 | public func sequencedFlatMapEach( 43 | on eventLoop: any EventLoop, 44 | _ transform: @escaping (_ element: Element) -> EventLoopFuture 45 | ) -> EventLoopFuture<[Result]> { 46 | return self.reduce(eventLoop.future([])) { fut, elem in fut.flatMap { res in transform(elem).map { res + [$0] } } } 47 | } 48 | 49 | /// An overload of `sequencedFlatMapEach(on:_:)` which returns a `Void` future instead 50 | /// of `[Void]` when the result type of the transform closure is `Void`. 51 | public func sequencedFlatMapEach( 52 | on eventLoop: any EventLoop, 53 | _ transform: @escaping (_ element: Element) -> EventLoopFuture 54 | ) -> EventLoopFuture { 55 | return self.reduce(eventLoop.future()) { fut, elem in fut.flatMap { transform(elem) } } 56 | } 57 | 58 | /// Variant of `sequencedFlatMapEach(on:_:)` which provides `compactMap()` semantics 59 | /// by allowing result values to be `nil`. Such results are not included in the 60 | /// output array. 61 | public func sequencedFlatMapEachCompact( 62 | on eventLoop: any EventLoop, 63 | _ transform: @escaping (_ element: Element) -> EventLoopFuture 64 | ) -> EventLoopFuture<[Result]> { 65 | return self.reduce(eventLoop.future([])) { fut, elem in fut.flatMap { res in transform(elem).map { res + [$0].compactMap { $0 } } } } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/Future+Optional.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopFuture { 4 | /// Calls a closure on an optional value that is wrapped in an `EventLoopFuture` if it exists. 5 | /// 6 | /// let optional = eventLoop.future(Optional.some(42)) 7 | /// let some = optional.optionalMap { int -> Float in 8 | /// return int * 3.14 9 | /// } 10 | /// // some: EventLoopFuture(Optional(131.88)) 11 | /// 12 | /// - parameters: 13 | /// - closure: The closure function that the optional value will be passed into. 14 | /// - unwrapped: The unwrapped optional value. 15 | /// - returns: The result of the closure passed into the method (or `nil`), wrapped in an `EventLoopFuture`. 16 | public func optionalMap( 17 | _ closure: @escaping (_ unwrapped: Wrapped) -> Result? 18 | ) -> EventLoopFuture where Value == Optional { 19 | return self.map { $0.flatMap(closure) } 20 | } 21 | 22 | /// Calls a closure on an optional value in an `EventLoopFuture` if it exists. 23 | /// 24 | /// let optional = eventLoop.future(Optiona.some(42)) 25 | /// let some = optional.optionalFlatMap { int -> EventLoopFuture in 26 | /// return int * 3.14 27 | /// } 28 | /// // some: EventLoopFuture(Optional(131.88)) 29 | /// 30 | /// - parameters: 31 | /// - closure: The closure to call on the unwrapped optional value. 32 | /// - unwrapped: The optional's value, unwrapped. 33 | /// - returns: The result of the closure if the optional was unwrapped, or nil if it wasn't, wrapped in an `EventLoopFuture`. 34 | public func optionalFlatMap( 35 | _ closure: @escaping (_ unwrapped: Wrapped) -> EventLoopFuture 36 | ) -> EventLoopFuture where Value == Optional { 37 | return self.flatMap { optional in 38 | guard let future = optional.map(closure) else { 39 | return self.eventLoop.makeSucceededFuture(nil) 40 | } 41 | 42 | return future.map(Optional.init) 43 | } 44 | } 45 | 46 | /// Calls a closure that returns an optional future on an optional value in an `EventLoopFuture` if it exists. 47 | /// 48 | /// let optional = eventLoop.future(Optiona.some(42)) 49 | /// let some = optional.optionalFlatMap { int -> EventLoopFuture in 50 | /// return int * Optional.some(3.14) 51 | /// } 52 | /// // some: EventLoopFuture(Optional(131.88)) 53 | /// 54 | /// - parameters: 55 | /// - closure: The closure to call on the unwrapped optional value. 56 | /// - unwrapped: The optional's value, unwrapped. 57 | /// - returns: The result of the closure if the optional was unwrapped, or nil if it wasn't, wrapped in an `EventLoopFuture`. 58 | public func optionalFlatMap( 59 | _ closure: @escaping (_ unwrapped: Wrapped) -> EventLoopFuture 60 | ) -> EventLoopFuture where Value == Optional 61 | { 62 | return self.flatMap { optional in 63 | return optional.flatMap(closure)?.map { $0 } ?? self.eventLoop.makeSucceededFuture(nil) 64 | } 65 | } 66 | 67 | /// Calls a throwing closure on an optional value in an `EventLoopFuture` if it exists. 68 | /// 69 | /// let optional = eventLoop.future(Optional.some(42)) 70 | /// let some = optional.optionalFlatMapThrowing { int -> Float in 71 | /// return int * 3.14 72 | /// } 73 | /// // some: EventLoopFuture(Optional(131.88)) 74 | /// 75 | /// - parameters: 76 | /// - closure: The closure to call on the unwrapped optional value. 77 | /// - unwrapped: The optional's value, unwrapped. 78 | /// - returns: The result of the closure if the optional was unwrapped, or nil if it wasn't, wrapped in an `EventLoopFuture`. 79 | public func optionalFlatMapThrowing( 80 | _ closure: @escaping (_ unwrapped: Wrapped) throws -> Result? 81 | ) -> EventLoopFuture where Value == Optional 82 | { 83 | return self.flatMapThrowing { optional in 84 | return try optional.flatMap(closure) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/Future+Nonempty.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopFuture { 4 | /// Checks that the future's value (if any) returns `false` for `.isEmpty`. If the check fails, the provided error 5 | /// is thrown. 6 | public func nonempty(orError error: @escaping @autoclosure () -> E) -> EventLoopFuture where Value: Collection { 7 | return self.guard({ !$0.isEmpty }, else: error()) 8 | } 9 | 10 | /// Checks that the future's value (if any) returns `false` for `.isEmpty`. If the check fails, a new future with 11 | /// the provided alternate value is returned. Otherwise, the provided normal `map()` callback is invoked. 12 | public func nonemptyMap( 13 | or alternate: @escaping @autoclosure () -> NewValue, 14 | _ transform: @escaping (Value) -> NewValue 15 | ) -> EventLoopFuture where Value: Collection { 16 | return self.map { !$0.isEmpty ? transform($0) : alternate() } 17 | } 18 | 19 | /// Checks that the future's value (if any) returns `false` for `.isEmpty`. If the check fails, a new future with 20 | /// an empty array as its value is returned. Otherwise, the provided normal `map()` callback is invoked. The 21 | /// callback's return type must be an `Array` or a `RangeReplaceableCollection`. 22 | public func nonemptyMap( 23 | _ transform: @escaping (Value) -> NewValue 24 | ) -> EventLoopFuture where Value: Collection, NewValue: RangeReplaceableCollection { 25 | return self.nonemptyMap(or: .init(), transform) 26 | } 27 | 28 | /// Checks that the future's value (if any) returns `false` for `.isEmpty`. If the check fails, a new future with 29 | /// the provided alternate value is returned. Otherwise, the provided normal `flatMapThrowing()` callback is 30 | /// invoked. 31 | public func nonemptyFlatMapThrowing( 32 | or alternate: @escaping @autoclosure () -> NewValue, 33 | _ transform: @escaping (Value) throws -> NewValue 34 | ) -> EventLoopFuture where Value: Collection { 35 | return self.flatMapThrowing { !$0.isEmpty ? try transform($0) : alternate() } 36 | } 37 | 38 | /// Checks that the future's value (if any) returns `false` for `.isEmpty`. If the check fails, a new future with 39 | /// an empty array as its value is returned. Otherwise, the provided normal `flatMapThrowing()` callback is 40 | /// invoked. The callback's return type must be an `Array` or a `RangeReplaceableCollection`. 41 | public func nonemptyFlatMapThrowing( 42 | _ transform: @escaping (Value) throws -> NewValue 43 | ) -> EventLoopFuture where Value: Collection, NewValue: RangeReplaceableCollection { 44 | return self.nonemptyFlatMapThrowing(or: .init(), transform) 45 | } 46 | 47 | /// Checks that the future's value (if any) returns `false` for `.isEmpty`. If the check fails, a new future with 48 | /// the provided alternate value is returned. Otherwise, the provided normal `flatMap()` callback is invoked. 49 | public func nonemptyFlatMap( 50 | or alternate: @escaping @autoclosure () -> NewValue, 51 | _ transform: @escaping (Value) -> EventLoopFuture 52 | ) -> EventLoopFuture where Value: Collection { 53 | return self.nonemptyFlatMap(orFlat: self.eventLoop.makeSucceededFuture(alternate()), transform) 54 | } 55 | 56 | /// Checks that the future's value (if any) returns `false` for `.isEmpty`. If the check fails, the provided 57 | /// alternate future is returned. Otherwise, the provided normal `flatMap()` callback is invoked. 58 | public func nonemptyFlatMap( 59 | orFlat alternate: @escaping @autoclosure () -> EventLoopFuture, 60 | _ transform: @escaping (Value) -> EventLoopFuture 61 | ) -> EventLoopFuture where Value: Collection { 62 | return self.flatMap { !$0.isEmpty ? transform($0) : alternate() } 63 | } 64 | 65 | /// Checks that the future's value (if any) returns `false` for `.isEmpty`. If the check fails, a new future with 66 | /// an empty array as its value is returned. Otherwise, the provided normal `flatMap()` callback is invoked. The 67 | /// callback's returned future must have a value type that is an `Array` or a `RangeReplaceableCollection`. 68 | public func nonemptyFlatMap( 69 | _ transform: @escaping (Value) -> EventLoopFuture 70 | ) -> EventLoopFuture where Value: Collection, NewValue: RangeReplaceableCollection { 71 | return self.nonemptyFlatMap(or: .init(), transform) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/FutureOperatorsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import AsyncKit 3 | import NIOCore 4 | 5 | final class FutureOperatorTests: AsyncKitTestCase { 6 | func testAddition() throws { 7 | var future1 = eventLoop.makeSucceededFuture(8) 8 | let future2 = eventLoop.makeSucceededFuture(5) 9 | 10 | XCTAssertEqual(try (future1 + future2).wait(), 13) 11 | 12 | future1 += future2 13 | XCTAssertEqual(try future1.wait(), 13) 14 | 15 | var arrayFuture1 = eventLoop.makeSucceededFuture([1, 2, 3]) 16 | let arrayFuture2 = eventLoop.makeSucceededFuture([4, 5, 6]) 17 | 18 | XCTAssertEqual(try (arrayFuture1 + arrayFuture2).wait(), [1, 2, 3, 4, 5, 6]) 19 | XCTAssertNotEqual(try (arrayFuture1 + arrayFuture2).wait(), try (arrayFuture2 + arrayFuture1).wait()) 20 | 21 | arrayFuture1 += arrayFuture2 22 | XCTAssertEqual(try arrayFuture1.wait(), [1, 2, 3, 4, 5, 6]) 23 | } 24 | 25 | func testSubtraction() throws { 26 | var future1 = eventLoop.makeSucceededFuture(8) 27 | let future2 = eventLoop.makeSucceededFuture(5) 28 | 29 | XCTAssertEqual(try (future1 - future2).wait(), 3) 30 | 31 | future1 -= future2 32 | XCTAssertEqual(try future1.wait(), 3) 33 | 34 | var arrayFuture1 = eventLoop.makeSucceededFuture([1, 2, 3, 4, 5, 6]) 35 | let arrayFuture2 = eventLoop.makeSucceededFuture([4, 5, 6]) 36 | 37 | XCTAssertEqual(try (arrayFuture1 - arrayFuture2).wait(), [1, 2, 3]) 38 | 39 | arrayFuture1 -= arrayFuture2 40 | XCTAssertEqual(try arrayFuture1.wait(), [1, 2, 3]) 41 | } 42 | 43 | func testMultiplication() throws { 44 | var future1 = eventLoop.makeSucceededFuture(8) 45 | let future2 = eventLoop.makeSucceededFuture(5) 46 | 47 | XCTAssertEqual(try (future1 * future2).wait(), 40) 48 | 49 | future1 *= future2 50 | XCTAssertEqual(try future1.wait(), 40) 51 | } 52 | 53 | func testModulo() throws { 54 | var future1 = eventLoop.makeSucceededFuture(8) 55 | let future2 = eventLoop.makeSucceededFuture(5) 56 | 57 | XCTAssertEqual(try (future1 % future2).wait(), 3) 58 | 59 | future1 %= future2 60 | XCTAssertEqual(try future1.wait(), 3) 61 | } 62 | 63 | func testDivision() throws { 64 | var future1 = eventLoop.makeSucceededFuture(40) 65 | let future2 = eventLoop.makeSucceededFuture(5) 66 | 67 | XCTAssertEqual(try (future1 / future2).wait(), 8) 68 | 69 | future1 /= future2 70 | XCTAssertEqual(try future1.wait(), 8) 71 | } 72 | 73 | func testComparison() throws { 74 | let future1 = eventLoop.makeSucceededFuture(8) 75 | let future2 = eventLoop.makeSucceededFuture(5) 76 | let future3 = eventLoop.makeSucceededFuture(5) 77 | 78 | XCTAssert(try (future2 < future1).wait()) 79 | XCTAssert(try (future2 <= future3).wait()) 80 | XCTAssert(try (future2 >= future3).wait()) 81 | XCTAssert(try (future1 > future3).wait()) 82 | } 83 | 84 | func testBitshifts() throws { 85 | var future1 = eventLoop.makeSucceededFuture(255) 86 | let future2 = eventLoop.makeSucceededFuture(16) 87 | 88 | XCTAssertEqual(try (future1 << future2).wait(), 16711680) 89 | 90 | future1 <<= future2 91 | XCTAssertEqual(try future1.wait(), 16711680) 92 | 93 | var future3 = eventLoop.makeSucceededFuture(16711680) 94 | let future4 = eventLoop.makeSucceededFuture(16) 95 | 96 | XCTAssertEqual(try (future3 >> future4).wait(), 255) 97 | 98 | future3 >>= future4 99 | XCTAssertEqual(try future3.wait(), 255) 100 | } 101 | 102 | func testAND() throws { 103 | var future1 = eventLoop.makeSucceededFuture(200) 104 | let future2 = eventLoop.makeSucceededFuture(500) 105 | 106 | XCTAssertEqual(try (future1 & future2).wait(), 192) 107 | 108 | future1 &= future2 109 | XCTAssertEqual(try future1.wait(), 192) 110 | } 111 | 112 | func testXOR() throws { 113 | var future1 = eventLoop.makeSucceededFuture(8) 114 | let future2 = eventLoop.makeSucceededFuture(5) 115 | 116 | XCTAssertEqual(try (future1 ^ future2).wait(), 13) 117 | 118 | future1 ^= future2 119 | XCTAssertEqual(try future1.wait(), 13) 120 | } 121 | 122 | func testOR() throws { 123 | var future1 = eventLoop.makeSucceededFuture(8) 124 | let future2 = eventLoop.makeSucceededFuture(5) 125 | 126 | XCTAssertEqual(try (future1 | future2).wait(), 13) 127 | 128 | future1 |= future2 129 | XCTAssertEqual(try future1.wait(), 13) 130 | } 131 | 132 | func testNOT() throws { 133 | let future1: EventLoopFuture = eventLoop.makeSucceededFuture(0b00001111) 134 | XCTAssertEqual(try ~future1.wait(), 0b11110000) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/EventLoopFutureQueue.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | /// Allows you to queue closures that produce an `EventLoopFuture`, so each future only gets run if the previous ones complete, succeed, or fail. 4 | public final class EventLoopFutureQueue { 5 | 6 | /// Under what conditions an appended closure should be run. 7 | public enum ContinueCondition { 8 | 9 | /// Run closure on the previous future's success. 10 | case success 11 | 12 | /// Run closure on the previous future's failure. 13 | case failure 14 | 15 | /// Run closure on the previous future's completion. 16 | case complete 17 | } 18 | 19 | /// Errors that get propogated based on a future's completion status and the next appended closure's continuation condition. 20 | public enum ContinueError: Error, CustomStringConvertible { 21 | 22 | /// A previous future failed with an error, which we don't desire. 23 | case previousError(any Error) 24 | 25 | /// A previous future succeeded, which we don't desire. 26 | case previousSuccess 27 | 28 | /// A textual representation of the error. 29 | /// 30 | /// In the case of a `.previousError` case, the result will be flattened to a single `.previousError(error)`, 31 | /// instead of being nested _n_ cases deep `.previousError(.previousError(.previousError(error)))`. 32 | public var description: String { 33 | switch self { 34 | case .previousSuccess: return "previousSuccess" 35 | case let .previousError(error): 36 | if let sub = error as? ContinueError { 37 | return sub.description 38 | } else { 39 | return "previousError(\(error))" 40 | } 41 | } 42 | } 43 | } 44 | 45 | /// The event loop that all the futures's completions are handled on. 46 | public let eventLoop: any EventLoop 47 | 48 | /// The current waiter future. 49 | private var current: EventLoopFuture 50 | 51 | /// Create a new `EventLoopFutureQueue` on a given event loop. 52 | /// 53 | /// - Parameter eventLoop: The event loop that all the futures's completions are handled on. 54 | public init(eventLoop: any EventLoop) { 55 | self.eventLoop = eventLoop 56 | self.current = eventLoop.makeSucceededFuture(()) 57 | } 58 | 59 | /// Adds another `EventLoopFuture` producing closure to be run as soon as all previously queued future have completed, succeeded, or failed. 60 | /// 61 | /// let model: EventLoopFuture = queue.append(generator: { Model.query(on: database).first() }) 62 | /// 63 | /// - Parameters: 64 | /// - next: The condition that the previous future(s) must meet on thier completion for the appended future to be run. 65 | /// The default value is `.complete`. 66 | /// - generator: The closure that produces the `EventLoopFuture`. We need a closure because otherwise the 67 | /// future starts running right away and the queuing doesn't do you any good. 68 | /// 69 | /// - Returns: The resulting future from the `generator` closure passed in. 70 | public func append( 71 | onPrevious next: ContinueCondition = .complete, 72 | generator: @escaping () -> EventLoopFuture 73 | ) -> EventLoopFuture { 74 | return self.eventLoop.flatSubmit { 75 | let promise = self.eventLoop.makePromise(of: Void.self) 76 | 77 | switch next { 78 | case .success: 79 | self.current.whenComplete { result in 80 | switch result { 81 | case .success: promise.succeed(()) 82 | case let .failure(error): promise.fail(ContinueError.previousError(error)) 83 | } 84 | } 85 | case .failure: 86 | self.current.whenComplete { result in 87 | switch result { 88 | case .success: promise.fail(ContinueError.previousSuccess) 89 | case .failure: promise.succeed(()) 90 | } 91 | } 92 | case .complete: 93 | self.current.whenComplete { _ in promise.succeed(()) } 94 | } 95 | 96 | let next = promise.futureResult.flatMap { generator() } 97 | self.current = next.map { _ in () } 98 | return next 99 | } 100 | } 101 | 102 | /// An overload for `append(generator:runningOn:)` that takes in an `EventLoopFuture` as an auto closure to provide a better 1-liner API. 103 | /// 104 | /// let model: EventLoopFuture = queue.append(Model.query(on: database).first()) 105 | /// 106 | /// - Parameters: 107 | /// - generator: The statement that will produce an `EventLoopFuture`. 108 | /// This will automatically get wrapped in a closure. 109 | /// - next: The condition that the previous future(s) must meet on their completion for the appended future to be run. 110 | /// The default value is `.complete`. 111 | /// 112 | /// - Returns: The future passed into the `generator` parameter. 113 | public func append( 114 | _ generator: @autoclosure @escaping () -> EventLoopFuture, 115 | runningOn next: ContinueCondition = .complete 116 | ) -> EventLoopFuture { 117 | self.append(onPrevious: next, generator: generator) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/AsyncKit/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``AsyncKit`` 2 | 3 | @Metadata { 4 | @TitleHeading(Package) 5 | } 6 | 7 | Provides a set of utilities for working with EventLoopFutures and other pre-Concurrency support APIs. 8 | 9 | ## Overview 10 | 11 | _**AsyncKit is a legacy package; its use is not recommended in new projects.**_ 12 | 13 | AsyncKit provides a number of extensions to both Swift's Concurrency primitives and NIO's futures to make working with them easier. The long-term goal is to migrate away from this package as Swift Concurrency adds support for working in an asynchronous environment easy. 14 | 15 | See references below for usage details. 16 | 17 | ## Topics 18 | 19 | ### Legacy connection pools 20 | 21 | - ``EventLoopConnectionPool`` 22 | - ``EventLoopGroupConnectionPool`` 23 | - ``ConnectionPoolSource`` 24 | - ``ConnectionPoolItem`` 25 | - ``ConnectionPoolError`` 26 | - ``ConnectionPoolTimeoutError`` 27 | 28 | ### Optionals 29 | 30 | - ``strictMap(_:_:)`` 31 | - ``strictMap(_:_:_:)`` 32 | - ``strictMap(_:_:_:_:)`` 33 | - ``strictMap(_:_:_:_:_:)`` 34 | - ``strictMap(_:_:_:_:_:_:)`` 35 | - ``strictMap(_:_:_:_:_:_:_:)`` 36 | - ``strictMap(_:_:_:_:_:_:_:_:)`` 37 | - ``strictMap(_:_:_:_:_:_:_:_:_:)`` 38 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:)`` 39 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:)`` 40 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:)`` 41 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:_:)`` 42 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:_:_:)`` 43 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:)`` 44 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:)`` 45 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:)`` 46 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:)`` 47 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:)`` 48 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:)`` 49 | - ``strictMap(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:)`` 50 | 51 | ### EventLoop and EventLoopGroup 52 | 53 | - ``NIOCore/EventLoop/flatten(_:)-6gsl5`` 54 | - ``NIOCore/EventLoop/flatten(_:)-7tski`` 55 | - ``NIOCore/EventLoopGroup/future()`` 56 | - ``NIOCore/EventLoopGroup/future(error:)`` 57 | - ``NIOCore/EventLoopGroup/future(_:)`` 58 | - ``NIOCore/EventLoopGroup/future(result:)`` 59 | - ``NIOCore/EventLoopGroup/tryFuture(_:)`` 60 | 61 | ### EventLoopFuture 62 | 63 | - ``EventLoopFutureQueue`` 64 | - ``NIOCore/EventLoopFuture/mapEach(_:)-3wa2g`` 65 | - ``NIOCore/EventLoopFuture/mapEach(_:)-9cwjy`` 66 | - ``NIOCore/EventLoopFuture/mapEachCompact(_:)-2vkmo`` 67 | - ``NIOCore/EventLoopFuture/mapEachCompact(_:)-4sfs3`` 68 | - ``NIOCore/EventLoopFuture/mapEachFlat(_:)-9kgfr`` 69 | - ``NIOCore/EventLoopFuture/mapEachFlat(_:)-783z2`` 70 | - ``NIOCore/EventLoopFuture/flatMapEach(on:_:)-9yail`` 71 | - ``NIOCore/EventLoopFuture/flatMapEach(on:_:)-8yhpe`` 72 | - ``NIOCore/EventLoopFuture/flatMapEachCompact(on:_:)`` 73 | - ``NIOCore/EventLoopFuture/flatMapEachThrowing(_:)`` 74 | - ``NIOCore/EventLoopFuture/flatMapEachCompactThrowing(_:)`` 75 | - ``NIOCore/EventLoopFuture/sequencedFlatMapEach(_:)-29ak2`` 76 | - ``NIOCore/EventLoopFuture/sequencedFlatMapEach(_:)-6d82b`` 77 | - ``NIOCore/EventLoopFuture/sequencedFlatMapEachCompact(_:)`` 78 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:file:line:)`` 79 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:file:line:)`` 80 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:file:line:)`` 81 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:file:line:)`` 82 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:file:line:)`` 83 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:file:line:)`` 84 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:file:line:)`` 85 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:file:line:)`` 86 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:file:line:)`` 87 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 88 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 89 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 90 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 91 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 92 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 93 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 94 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 95 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 96 | - ``NIOCore/EventLoopFuture/whenTheySucceed(_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:_:file:line:)`` 97 | - ``NIOCore/EventLoopFuture/guard(_:else:)`` 98 | - ``NIOCore/EventLoopFuture/flatMapAlways(file:line:_:)`` 99 | - ``NIOCore/EventLoopFuture/nonempty(orError:)`` 100 | - ``NIOCore/EventLoopFuture/nonemptyMap(_:)`` 101 | - ``NIOCore/EventLoopFuture/nonemptyMap(or:_:)`` 102 | - ``NIOCore/EventLoopFuture/nonemptyFlatMapThrowing(_:)`` 103 | - ``NIOCore/EventLoopFuture/nonemptyFlatMapThrowing(or:_:)`` 104 | - ``NIOCore/EventLoopFuture/nonemptyFlatMap(_:)`` 105 | - ``NIOCore/EventLoopFuture/nonemptyFlatMap(or:_:)`` 106 | - ``NIOCore/EventLoopFuture/nonemptyFlatMap(orFlat:_:)`` 107 | - ``NIOCore/EventLoopFuture/optionalMap(_:)`` 108 | - ``NIOCore/EventLoopFuture/optionalFlatMap(_:)-1lhnd`` 109 | - ``NIOCore/EventLoopFuture/optionalFlatMap(_:)-1c1gn`` 110 | - ``NIOCore/EventLoopFuture/optionalFlatMapThrowing(_:)`` 111 | - ``NIOCore/EventLoopFuture/transform(to:)-7agus`` 112 | - ``NIOCore/EventLoopFuture/transform(to:)-3k9qv`` 113 | - ``NIOCore/EventLoopFuture/tryFlatMap(file:line:_:)`` 114 | 115 | ### EventLoopFuture operators 116 | 117 | - ``NIOCore/EventLoopFuture/+(_:_:)-48eo2`` 118 | - ``NIOCore/EventLoopFuture/+(_:_:)-78tv8`` 119 | - ``NIOCore/EventLoopFuture/+=(_:_:)-3854m`` 120 | - ``NIOCore/EventLoopFuture/+=(_:_:)-1t7oh`` 121 | 122 | 123 | 124 | 125 | - ``NIOCore/EventLoopFuture/*(_:_:)`` 126 | - ``NIOCore/EventLoopFuture/*=(_:_:)`` 127 | 128 | 129 | - ``NIOCore/EventLoopFuture/%(_:_:)`` 130 | - ``NIOCore/EventLoopFuture/%=(_:_:)`` 131 | - ``NIOCore/EventLoopFuture/<(_:_:)`` 132 | - ``NIOCore/EventLoopFuture/<=(_:_:)`` 133 | - ``NIOCore/EventLoopFuture/>(_:_:)`` 134 | - ``NIOCore/EventLoopFuture/>=(_:_:)`` 135 | - ``NIOCore/EventLoopFuture/<<(_:_:)`` 136 | - ``NIOCore/EventLoopFuture/<<=(_:_:)`` 137 | - ``NIOCore/EventLoopFuture/>>(_:_:)`` 138 | - ``NIOCore/EventLoopFuture/>>=(_:_:)`` 139 | - ``NIOCore/EventLoopFuture/&(_:_:)`` 140 | - ``NIOCore/EventLoopFuture/&=(_:_:)`` 141 | - ``NIOCore/EventLoopFuture/|(_:_:)`` 142 | - ``NIOCore/EventLoopFuture/|=(_:_:)`` 143 | - ``NIOCore/EventLoopFuture/^(_:_:)`` 144 | - ``NIOCore/EventLoopFuture/^=(_:_:)`` 145 | - ``NIOCore/EventLoopFuture/~(_:)`` 146 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/FutureOperators.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | // MARK: - Numeric 4 | 5 | extension EventLoopFuture where Value: Numeric { 6 | /// Adds two futures and produces their sum 7 | public static func + (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 + $1 } } 8 | 9 | /// Adds two futures and stores the result in the left-hand-side variable 10 | public static func += (lhs: inout EventLoopFuture, rhs: EventLoopFuture) { lhs = lhs + rhs } 11 | 12 | /// Subtracts one future from another and produces their difference 13 | public static func - (_ lhs: EventLoopFuture, _ rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 - $1 } } 14 | 15 | /// Subtracts the second future from the first and stores the difference in the left-hand-side variable 16 | public static func -= (_ lhs: inout EventLoopFuture, _ rhs: EventLoopFuture) { lhs = lhs - rhs } 17 | 18 | /// Multiplies two futures and produces their product 19 | public static func * (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 * $1 } } 20 | 21 | /// Multiplies two futures and stores the result in the left-hand-side variable 22 | public static func *= (lhs: inout EventLoopFuture, rhs: EventLoopFuture) { lhs = lhs * rhs } 23 | } 24 | 25 | // MARK: - Array 26 | 27 | extension EventLoopFuture { 28 | /// Adds two futures and produces their sum 29 | public static func + (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture where Value == Array, T: Equatable { lhs.and(rhs).map { $0 + $1 } } 30 | 31 | /// Adds two futures and stores the result in the left-hand-side variable 32 | public static func += (lhs: inout EventLoopFuture, rhs: EventLoopFuture) where Value == Array, T: Equatable { lhs = lhs + rhs } 33 | 34 | /// Subtracts one future from another and produces their difference 35 | public static func - (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture where Value == Array, T: Equatable { 36 | lhs.and(rhs).map { l, r in l.filter { !r.contains($0) } } 37 | } 38 | 39 | /// Subtracts the second future from the first and stores the difference in the left-hand-side variable 40 | public static func -= (lhs: inout EventLoopFuture, rhs: EventLoopFuture) where Value == Array, T: Equatable { lhs = lhs - rhs } 41 | } 42 | 43 | // MARK: - BinaryInteger (division) 44 | 45 | extension EventLoopFuture where Value: BinaryInteger { 46 | /// Returns the quotient of dividing the first future by the second 47 | public static func / (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 / $1 } } 48 | 49 | /// Divides the first future by the second and stores the quotient in the left-hand-side variable 50 | public static func /= (lhs: inout EventLoopFuture, rhs: EventLoopFuture) { lhs = lhs / rhs } 51 | 52 | /// Returns the remainder of dividing the first future by the second 53 | public static func % (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 % $1 } } 54 | 55 | /// Divides the first future by the second and stores the remainder in the left-hand-side variable 56 | public static func %= (lhs: inout EventLoopFuture, rhs: EventLoopFuture) { lhs = lhs % rhs } 57 | } 58 | 59 | // MARK: - BinaryInteger (comparison) 60 | 61 | extension EventLoopFuture where Value: BinaryInteger { 62 | /// Returns a Boolean value indicating whether the value of the first argument is less than that of the second argument 63 | public static func < (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 < $1 } } 64 | 65 | /// Returns a Boolean value indicating whether the value of the first argument is less than or equal to that of the second argument 66 | public static func <= (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 <= $1 } } 67 | 68 | /// Returns a Boolean value indicating whether the value of the first argument is greater than or equal to that of the second argument 69 | public static func >= (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 >= $1 } } 70 | 71 | /// Returns a Boolean value indicating whether the value of the first argument is greater than that of the second argument 72 | public static func > (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 > $1 } } 73 | } 74 | 75 | // MARK: - BinaryInteger (bitwise) 76 | 77 | extension EventLoopFuture where Value: BinaryInteger { 78 | /// Returns the result of shifting a future’s binary representation the specified number of digits to the left 79 | public static func << (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 << $1 } } 80 | 81 | /// Stores the result of shifting a future’s binary representation the specified number of digits to the left in the left-hand-side variable 82 | public static func <<= (lhs: inout EventLoopFuture, rhs: EventLoopFuture) { lhs = lhs << rhs } 83 | 84 | /// Returns the result of shifting a future’s binary representation the specified number of digits to the right 85 | public static func >> (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 >> $1 } } 86 | 87 | /// Stores the result of shifting a future’s binary representation the specified number of digits to the right in the left-hand-side variable 88 | public static func >>= (lhs: inout EventLoopFuture, rhs: EventLoopFuture) { lhs = lhs >> rhs } 89 | 90 | /// Returns the result of performing a bitwise AND operation on the two given futures 91 | public static func & (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 & $1 } } 92 | 93 | /// Stores the result of performing a bitwise AND operation on the two given futures in the left-hand-side variable 94 | public static func &= (lhs: inout EventLoopFuture, rhs: EventLoopFuture) { lhs = lhs & rhs } 95 | 96 | /// Returns the result of performing a bitwise OR operation on the two given futures 97 | public static func | (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 | $1 } } 98 | 99 | /// Stores the result of performing a bitwise OR operation on the two given futures in the left-hand-side variable 100 | public static func |= (lhs: inout EventLoopFuture, rhs: EventLoopFuture) { lhs = lhs | rhs } 101 | 102 | /// Returns the result of performing a bitwise XOR operation on the two given futures 103 | public static func ^ (lhs: EventLoopFuture, rhs: EventLoopFuture) -> EventLoopFuture { lhs.and(rhs).map { $0 ^ $1 } } 104 | 105 | /// Stores the result of performing a bitwise XOR operation on the two given futures in the left-hand-side variable 106 | public static func ^= (lhs: inout EventLoopFuture, rhs: EventLoopFuture) { lhs = lhs ^ rhs } 107 | 108 | /// Returns the result of performing a bitwise NOT operation on the given future 109 | public static prefix func ~ (x: EventLoopFuture) -> EventLoopFuture { x.map { ~$0 } } 110 | } 111 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/AsyncConnectionPoolTests.swift: -------------------------------------------------------------------------------- 1 | import Atomics 2 | @preconcurrency import AsyncKit 3 | import XCTest 4 | import NIOConcurrencyHelpers 5 | import Logging 6 | import NIOCore 7 | import NIOEmbedded 8 | 9 | final class AsyncConnectionPoolTests: AsyncKitAsyncTestCase { 10 | func testPooling() async throws { 11 | let foo = FooDatabase() 12 | let pool = EventLoopConnectionPool( 13 | source: foo, 14 | maxConnections: 2, 15 | on: self.group.any() 16 | ) 17 | 18 | // make two connections 19 | let connA = try await pool.requestConnection().get() 20 | XCTAssertEqual(connA.isClosed, false) 21 | let connB = try await pool.requestConnection().get() 22 | XCTAssertEqual(connB.isClosed, false) 23 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 2) 24 | 25 | // try to make a third, but pool only supports 2 26 | let futureC = pool.requestConnection() 27 | let connC = ManagedAtomic(nil) 28 | futureC.whenSuccess { connC.store($0, ordering: .relaxed) } 29 | XCTAssertNil(connC.load(ordering: .relaxed)) 30 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 2) 31 | 32 | // release one of the connections, allowing the third to be made 33 | pool.releaseConnection(connB) 34 | let connCRet = try await futureC.get() 35 | XCTAssertNotNil(connC.load(ordering: .relaxed)) 36 | XCTAssert(connC.load(ordering: .relaxed) === connB) 37 | XCTAssert(connCRet === connC.load(ordering: .relaxed)) 38 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 2) 39 | 40 | // try to make a third again, with two active 41 | let futureD = pool.requestConnection() 42 | let connD = ManagedAtomic(nil) 43 | futureD.whenSuccess { connD.store($0, ordering: .relaxed) } 44 | XCTAssertNil(connD.load(ordering: .relaxed)) 45 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 2) 46 | 47 | // this time, close the connection before releasing it 48 | try await connCRet.close().get() 49 | pool.releaseConnection(connC.load(ordering: .relaxed)!) 50 | let connDRet = try await futureD.get() 51 | XCTAssert(connD.load(ordering: .relaxed) !== connB) 52 | XCTAssert(connDRet === connD.load(ordering: .relaxed)) 53 | XCTAssertEqual(connD.load(ordering: .relaxed)?.isClosed, false) 54 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 3) 55 | 56 | try! await pool.close().get() 57 | } 58 | 59 | func testFIFOWaiters() async throws { 60 | let foo = FooDatabase() 61 | let pool = EventLoopConnectionPool( 62 | source: foo, 63 | maxConnections: 1, 64 | on: self.group.any() 65 | ) 66 | 67 | // * User A makes a request for a connection, gets connection number 1. 68 | let a_1 = pool.requestConnection() 69 | let a = try await a_1.get() 70 | 71 | // * User B makes a request for a connection, they are exhausted so he gets a promise. 72 | let b_1 = pool.requestConnection() 73 | 74 | // * User A makes another request for a connection, they are still exhausted so he gets a promise. 75 | let a_2 = pool.requestConnection() 76 | 77 | // * User A returns connection number 1. His previous request is fulfilled with connection number 1. 78 | pool.releaseConnection(a) 79 | 80 | // * User B gets his connection 81 | let b = try await b_1.get() 82 | XCTAssert(a === b) 83 | 84 | // * User B releases his connection 85 | pool.releaseConnection(b) 86 | 87 | // * User A's second connection request is fulfilled 88 | let c = try await a_2.get() 89 | XCTAssert(a === c) 90 | 91 | try! await pool.close().get() 92 | } 93 | 94 | func testConnectError() async throws { 95 | let db = ErrorDatabase() 96 | let pool = EventLoopConnectionPool( 97 | source: db, 98 | maxConnections: 1, 99 | on: self.group.any() 100 | ) 101 | 102 | do { 103 | _ = try await pool.requestConnection().get() 104 | XCTFail("should not have created connection") 105 | } catch _ as ErrorDatabase.Error { 106 | // pass 107 | } 108 | 109 | // test that we can still make another request even after a failed request 110 | do { 111 | _ = try await pool.requestConnection().get() 112 | XCTFail("should not have created connection") 113 | } catch _ as ErrorDatabase.Error { 114 | // pass 115 | } 116 | 117 | try! await pool.close().get() 118 | } 119 | 120 | func testPoolClose() async throws { 121 | let foo = FooDatabase() 122 | let pool = EventLoopConnectionPool( 123 | source: foo, 124 | maxConnections: 1, 125 | on: self.group.any() 126 | ) 127 | let _ = try await pool.requestConnection().get() 128 | let b = pool.requestConnection() 129 | try await pool.close().get() 130 | 131 | let c = pool.requestConnection() 132 | 133 | // check that waiters are failed 134 | do { 135 | _ = try await b.get() 136 | XCTFail("should not have created connection") 137 | } catch ConnectionPoolError.shutdown { 138 | // pass 139 | } 140 | 141 | // check that new requests fail 142 | do { 143 | _ = try await c.get() 144 | XCTFail("should not have created connection") 145 | } catch ConnectionPoolError.shutdown { 146 | // pass 147 | } 148 | } 149 | 150 | func testGracefulShutdownAsync() async throws { 151 | let foo = FooDatabase() 152 | let pool = EventLoopGroupConnectionPool( 153 | source: foo, 154 | maxConnectionsPerEventLoop: 2, 155 | on: self.group 156 | ) 157 | 158 | try await pool.shutdownAsync() 159 | var errorCaught = false 160 | 161 | do { 162 | try await pool.shutdownAsync() 163 | } catch { 164 | errorCaught = true 165 | XCTAssertEqual(error as? ConnectionPoolError, ConnectionPoolError.shutdown) 166 | } 167 | XCTAssertTrue(errorCaught) 168 | } 169 | 170 | func testShutdownWithHeldConnection() async throws { 171 | let foo = FooDatabase() 172 | let pool = EventLoopGroupConnectionPool( 173 | source: foo, 174 | maxConnectionsPerEventLoop: 2, 175 | on: self.group 176 | ) 177 | 178 | let connection = try await pool.requestConnection().get() 179 | 180 | try await pool.shutdownAsync() 181 | var errorCaught = false 182 | 183 | do { 184 | try await pool.shutdownAsync() 185 | } catch { 186 | errorCaught = true 187 | XCTAssertEqual(error as? ConnectionPoolError, ConnectionPoolError.shutdown) 188 | } 189 | XCTAssertTrue(errorCaught) 190 | 191 | let result1 = try await connection.eventLoop.submit { connection.isClosed }.get() 192 | XCTAssertFalse(result1) 193 | pool.releaseConnection(connection) 194 | let result2 = try await connection.eventLoop.submit { connection.isClosed }.get() 195 | XCTAssertTrue(result2) 196 | } 197 | 198 | func testEventLoopDelegation() async throws { 199 | let foo = FooDatabase() 200 | let pool = EventLoopGroupConnectionPool( 201 | source: foo, 202 | maxConnectionsPerEventLoop: 1, 203 | on: self.group 204 | ) 205 | 206 | for _ in 0..<500 { 207 | let eventLoop = self.group.any() 208 | let a = pool.requestConnection( 209 | on: eventLoop 210 | ).map { conn in 211 | XCTAssertTrue(eventLoop.inEventLoop) 212 | pool.releaseConnection(conn) 213 | } 214 | let b = pool.requestConnection( 215 | on: eventLoop 216 | ).map { conn in 217 | XCTAssertTrue(eventLoop.inEventLoop) 218 | pool.releaseConnection(conn) 219 | } 220 | _ = try await a.and(b).get() 221 | } 222 | 223 | try await pool.shutdownAsync() 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/EventLoopFutureQueueTests.swift: -------------------------------------------------------------------------------- 1 | import NIOConcurrencyHelpers 2 | import AsyncKit 3 | import XCTest 4 | import NIOCore 5 | 6 | final class EventLoopFutureQueueTests: AsyncKitTestCase { 7 | func testQueue() throws { 8 | let queue = EventLoopFutureQueue(eventLoop: self.eventLoop) 9 | var numbers: [Int] = [] 10 | let lock = NIOLock() 11 | 12 | let one = queue.append(generator: { 13 | self.eventLoop.slowFuture(1, sleeping: .random(in: 0.01 ... 0.5)).map { number -> Int in 14 | lock.withLockVoid { numbers.append(number) } 15 | return number 16 | } 17 | }) 18 | let two = queue.append(generator: { 19 | self.eventLoop.slowFuture(2, sleeping: .random(in: 0.01 ... 0.5)).map { number -> Int in 20 | lock.withLockVoid { numbers.append(number) } 21 | return number 22 | } 23 | }) 24 | let three = queue.append(generator: { 25 | self.eventLoop.slowFuture(3, sleeping: .random(in: 0.01 ... 0.5)).map { number -> Int in 26 | lock.withLockVoid { numbers.append(number) } 27 | return number 28 | } 29 | }) 30 | let four = queue.append(generator: { 31 | self.eventLoop.slowFuture(4, sleeping: .random(in: 0.01 ... 0.5)).map { number -> Int in 32 | lock.withLockVoid { numbers.append(number) } 33 | return number 34 | } 35 | }) 36 | let five = queue.append(generator: { 37 | self.eventLoop.slowFuture(5, sleeping: .random(in: 0.01 ... 0.5)).map { number -> Int in 38 | lock.withLockVoid { numbers.append(number) } 39 | return number 40 | } 41 | }) 42 | 43 | try XCTAssertEqual(one.wait(), 1) 44 | try XCTAssertEqual(two.wait(), 2) 45 | try XCTAssertEqual(three.wait(), 3) 46 | try XCTAssertEqual(four.wait(), 4) 47 | try XCTAssertEqual(five.wait(), 5) 48 | 49 | XCTAssertEqual(numbers, [1, 2, 3, 4, 5]) 50 | } 51 | 52 | func testAutoclosure() throws { 53 | let queue = EventLoopFutureQueue(eventLoop: self.eventLoop) 54 | var numbers: [Int] = [] 55 | let lock = NIOLock() 56 | 57 | let one = queue.append(self.eventLoop.slowFuture(1, sleeping: 0.25).map { num in lock.withLockVoid { numbers.append(num) } }) 58 | let two = queue.append(self.eventLoop.slowFuture(2, sleeping: 0).map { num in lock.withLockVoid { numbers.append(num) } }) 59 | let three = queue.append(self.eventLoop.slowFuture(3, sleeping: 0).map { num in lock.withLockVoid { numbers.append(num) } }) 60 | let four = queue.append(self.eventLoop.slowFuture(4, sleeping: 0.25).map { num in lock.withLockVoid { numbers.append(num) } }) 61 | let five = queue.append(self.eventLoop.slowFuture(5, sleeping: 0.25).map { num in lock.withLockVoid { numbers.append(num) } }) 62 | 63 | try XCTAssertNoThrow(one.wait()) 64 | try XCTAssertNoThrow(two.wait()) 65 | try XCTAssertNoThrow(three.wait()) 66 | try XCTAssertNoThrow(four.wait()) 67 | try XCTAssertNoThrow(five.wait()) 68 | 69 | XCTAssertEqual(numbers, [1, 2, 3, 4, 5]) 70 | } 71 | 72 | func testContinueOnSucceed() throws { 73 | let queue = EventLoopFutureQueue(eventLoop: self.eventLoop) 74 | 75 | let one = queue.append(self.eventLoop.slowFuture(1, sleeping: 0.25), runningOn: .success) 76 | let two = queue.append(self.eventLoop.slowFuture(2, sleeping: 0), runningOn: .success) 77 | let fail: EventLoopFuture = queue.append( 78 | self.eventLoop.makeFailedFuture(Failure.nope), 79 | runningOn: .success 80 | ) 81 | let three = queue.append(self.eventLoop.slowFuture(3, sleeping: 0.25), runningOn: .success) 82 | 83 | try XCTAssertEqual(one.wait(), 1) 84 | try XCTAssertEqual(two.wait(), 2) 85 | 86 | try XCTAssertThrowsError(fail.wait()) { error in 87 | XCTAssertEqual(error as? Failure, .nope) 88 | } 89 | try XCTAssertThrowsError(three.wait()) { error in 90 | guard case let EventLoopFutureQueue.ContinueError.previousError(fail) = error else { 91 | return XCTFail("Unexpected error \(error.localizedDescription)") 92 | } 93 | 94 | XCTAssertEqual(fail as? Failure, .nope) 95 | } 96 | } 97 | 98 | func testContinueOnFail() throws { 99 | let queue = EventLoopFutureQueue(eventLoop: self.eventLoop) 100 | 101 | let fail: EventLoopFuture = queue.append( 102 | self.eventLoop.makeFailedFuture(Failure.nope), 103 | runningOn: .success 104 | ) 105 | let one = queue.append(self.eventLoop.slowFuture(1, sleeping: 0.25), runningOn: .failure) 106 | let two = queue.append(self.eventLoop.slowFuture(2, sleeping: 0), runningOn: .success) 107 | let three = queue.append(self.eventLoop.slowFuture(3, sleeping: 0.25), runningOn: .failure) 108 | 109 | try XCTAssertEqual(one.wait(), 1) 110 | try XCTAssertEqual(two.wait(), 2) 111 | 112 | try XCTAssertThrowsError(fail.wait()) { error in 113 | XCTAssertEqual(error as? Failure, .nope) 114 | } 115 | try XCTAssertThrowsError(three.wait()) { error in 116 | guard case EventLoopFutureQueue.ContinueError.previousSuccess = error else { 117 | return XCTFail("Unexpected error \(error.localizedDescription)") 118 | } 119 | } 120 | } 121 | 122 | func testSimpleSequence() throws { 123 | let queue = EventLoopFutureQueue(eventLoop: self.eventLoop) 124 | let values = [1, 2, 3, 4, 5] 125 | 126 | let all = queue.append(each: values) { self.eventLoop.slowFuture($0, sleeping: 0.25) } 127 | 128 | try XCTAssertEqual(all.wait(), [1, 2, 3, 4, 5]) 129 | } 130 | 131 | func testVoidReturnSequence() throws { 132 | let queue = EventLoopFutureQueue(eventLoop: self.eventLoop) 133 | let values = 99..<135 134 | var output: [Int] = [] 135 | let all = queue.append(each: values) { v in self.eventLoop.slowFuture((), sleeping: 0).map { output.append(v) } } 136 | 137 | try XCTAssertNoThrow(all.wait()) 138 | XCTAssertEqual(Array(values), output) 139 | } 140 | 141 | func testSequenceWithFailure() throws { 142 | let queue = EventLoopFutureQueue(eventLoop: self.eventLoop) 143 | var completedResults: [Int] = [] 144 | 145 | let all = queue.append(each: [1, 2, 3, 4, 5, 6]) { (v: Int) -> EventLoopFuture in 146 | guard v < 3 || v > 5 else { 147 | return self.eventLoop.future(error: Failure.nope) 148 | } 149 | completedResults.append(v) 150 | return self.eventLoop.slowFuture(v, sleeping: 0.25) 151 | } 152 | 153 | try XCTAssertThrowsError(all.wait()) 154 | XCTAssertEqual(completedResults, [1, 2]) // Make sure we didn't run with value 6 155 | } 156 | 157 | func testVoidSequenceWithFailure() throws { 158 | let queue = EventLoopFutureQueue(eventLoop: self.eventLoop) 159 | let values = 99..<135 160 | var output: [Int] = [] 161 | let all = queue.append(each: values) { v -> EventLoopFuture in 162 | guard v < 104 || v > 110 else { 163 | return self.eventLoop.future(error: Failure.nope) 164 | } 165 | return self.eventLoop.slowFuture((), sleeping: 0).map { output.append(v) } 166 | } 167 | 168 | try XCTAssertThrowsError(all.wait()) 169 | XCTAssertEqual(Array(values).prefix(while: { $0 < 104 }), output) 170 | } 171 | 172 | func testEmptySequence() throws { 173 | let queue = EventLoopFutureQueue(eventLoop: self.eventLoop) 174 | var count = 0 175 | 176 | _ = queue.append(onPrevious: .success) { queue.eventLoop.tryFuture { count = 1 } } 177 | let all = queue.append(each: Array(), { _ in queue.eventLoop.tryFuture { count = 99 } }) 178 | 179 | try XCTAssertNoThrow(all.wait()) 180 | XCTAssertEqual(count, 1) 181 | } 182 | 183 | func testContinueErrorPreviousErrorDescription() throws { 184 | let error = EventLoopFutureQueue.ContinueError.previousError( 185 | EventLoopFutureQueue.ContinueError.previousError( 186 | EventLoopFutureQueue.ContinueError.previousError(Failure.nope) 187 | ) 188 | ) 189 | 190 | XCTAssertEqual(error.description, "previousError(nope)") 191 | } 192 | } 193 | 194 | fileprivate extension EventLoop { 195 | func slowFuture(_ value: T, sleeping: TimeInterval) -> EventLoopFuture { 196 | Thread.sleep(forTimeInterval: sleeping) 197 | return self.future(value) 198 | } 199 | } 200 | 201 | fileprivate enum Failure: Error, Equatable { 202 | case nope 203 | } 204 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/Future+CollectionTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncKit 2 | import XCTest 3 | import NIOCore 4 | import NIOConcurrencyHelpers 5 | 6 | extension EventLoopGroup { 7 | func spinAll(on eventLoop: any EventLoop) -> EventLoopFuture { 8 | assert(self.makeIterator().contains(where: { ObjectIdentifier($0) == ObjectIdentifier(eventLoop) })) 9 | 10 | return .andAllSucceed(self.makeIterator().map { $0.submit {} }, on: eventLoop) 11 | } 12 | } 13 | 14 | final class FutureCollectionTests: AsyncKitTestCase { 15 | func testMapEach() throws { 16 | let collection1 = self.eventLoop.makeSucceededFuture([1, 2, 3, 4, 5, 6, 7, 8, 9]) 17 | let times2 = collection1.mapEach { int -> Int in int * 2 } 18 | 19 | try XCTAssertEqual(times2.wait(), [2, 4, 6, 8, 10, 12, 14, 16, 18]) 20 | 21 | let collection2 = self.eventLoop.makeSucceededFuture(["a", "bb", "ccc", "dddd", "eeeee"]) 22 | let lengths = collection2.mapEach(\.count) 23 | 24 | try XCTAssertEqual(lengths.wait(), [1, 2, 3, 4, 5]) 25 | 26 | } 27 | 28 | func testMapEachCompact() throws { 29 | let collection1 = self.eventLoop.makeSucceededFuture(["one", "2", "3", "4", "five", "^", "7"]) 30 | let times2 = collection1.mapEachCompact(Int.init) 31 | 32 | try XCTAssertEqual(times2.wait(), [2, 3, 4, 7]) 33 | 34 | let collection2 = self.eventLoop.makeSucceededFuture(["asdf", "qwer", "zxcv", ""]) 35 | let letters = collection2.mapEachCompact(\.first) 36 | try XCTAssertEqual(letters.wait(), ["a", "q", "z"]) 37 | } 38 | 39 | func testMapEachFlat() throws { 40 | let collection1 = self.eventLoop.makeSucceededFuture([[1, 2, 3], [9, 8, 7], [], [0]]) 41 | let flat1 = collection1.mapEachFlat { $0 } 42 | 43 | try XCTAssertEqual(flat1.wait(), [1, 2, 3, 9, 8, 7, 0]) 44 | 45 | let collection2 = self.eventLoop.makeSucceededFuture(["ABC", "123", "👩‍👩‍👧‍👧"]) 46 | let flat2 = collection2.mapEachFlat(\.utf8CString).mapEach(UInt8.init) 47 | 48 | try XCTAssertEqual(flat2.wait(), [ 49 | 0x41, 0x42, 0x43, 0x00, 0x31, 0x32, 0x33, 0x00, 0xf0, 0x9f, 0x91, 0xa9, 50 | 0xe2, 0x80, 0x8d, 0xf0, 0x9f, 0x91, 0xa9, 0xe2, 0x80, 0x8d, 0xf0, 0x9f, 51 | 0x91, 0xa7, 0xe2, 0x80, 0x8d, 0xf0, 0x9f, 0x91, 0xa7, 0x00 52 | ]) 53 | 54 | } 55 | 56 | func testFlatMapEach() throws { 57 | let collection = eventLoop.makeSucceededFuture([1, 2, 3, 4, 5, 6, 7, 8, 9]) 58 | let times2 = collection.flatMapEach(on: self.eventLoop) { int -> EventLoopFuture in 59 | return self.eventLoop.makeSucceededFuture(int * 2) 60 | } 61 | 62 | try XCTAssertEqual(times2.wait(), [2, 4, 6, 8, 10, 12, 14, 16, 18]) 63 | } 64 | 65 | func testFlatMapEachVoid() throws { 66 | let expectation = XCTestExpectation(description: "futures all succeeded") 67 | expectation.assertForOverFulfill = true 68 | expectation.expectedFulfillmentCount = 9 69 | 70 | let collection = self.eventLoop.makeSucceededFuture(1 ... 9) 71 | let nothing = collection.flatMapEach(on: self.eventLoop) { _ in expectation.fulfill(); return self.eventLoop.makeSucceededVoidFuture() } 72 | 73 | _ = try nothing.wait() 74 | XCTAssertEqual(XCTWaiter().wait(for: [expectation], timeout: 10.0), .completed) 75 | } 76 | 77 | func testFlatMapEachCompact() throws { 78 | let collection = self.eventLoop.makeSucceededFuture(["one", "2", "3", "4", "five", "^", "7"]) 79 | let times2 = collection.flatMapEachCompact(on: self.eventLoop) { int -> EventLoopFuture in 80 | return self.eventLoop.makeSucceededFuture(Int(int)) 81 | } 82 | 83 | try XCTAssertEqual(times2.wait(), [2, 3, 4, 7]) 84 | } 85 | 86 | func testFlatMapEachThrowing() throws { 87 | struct SillyRangeError: Error {} 88 | let collection = eventLoop.makeSucceededFuture([1, 2, 3, 4, 5, 6, 7, 8, 9]) 89 | let times2 = collection.flatMapEachThrowing { int -> Int in 90 | guard int < 8 else { throw SillyRangeError() } 91 | return int * 2 92 | } 93 | 94 | XCTAssertThrowsError(try times2.wait()) 95 | } 96 | 97 | func testFlatMapEachCompactThrowing() throws { 98 | struct SillyRangeError: Error {} 99 | let collection = self.eventLoop.makeSucceededFuture(["one", "2", "3", "4", "five", "^", "7"]) 100 | let times2 = collection.flatMapEachCompactThrowing { str -> Int? in Int(str) } 101 | let times2Badly = collection.flatMapEachCompactThrowing { str -> Int? in 102 | guard let result = Int(str) else { return nil } 103 | guard result < 4 else { throw SillyRangeError() } 104 | return result 105 | } 106 | 107 | try XCTAssertEqual(times2.wait(), [2, 3, 4, 7]) 108 | XCTAssertThrowsError(try times2Badly.wait()) 109 | } 110 | 111 | func testSequencedFlatMapEach() throws { 112 | struct SillyRangeError: Error {} 113 | var value = 0 114 | let lock = NIOLock() 115 | let collection = eventLoop.makeSucceededFuture([1, 2, 3, 4, 5, 6, 7, 8, 9]) 116 | let times2 = collection.sequencedFlatMapEach { int -> EventLoopFuture in 117 | lock.withLock { value = Swift.max(value, int) } 118 | guard int < 5 else { return self.group.spinAll(on: self.eventLoop).flatMapThrowing { throw SillyRangeError() } } 119 | return self.eventLoop.makeSucceededFuture(int * 2) 120 | } 121 | 122 | XCTAssertThrowsError(try times2.wait()) 123 | XCTAssertLessThan(lock.withLock { value }, 6) 124 | } 125 | 126 | func testSequencedFlatMapVoid() throws { 127 | struct SillyRangeError: Error {} 128 | var value = 0 129 | let lock = NIOLock() 130 | let collection = eventLoop.makeSucceededFuture([1, 2, 3, 4, 5, 6, 7, 8, 9]) 131 | let times2 = collection.sequencedFlatMapEach { int -> EventLoopFuture in 132 | lock.withLock { value = Swift.max(value, int) } 133 | guard int < 5 else { return self.group.spinAll(on: self.eventLoop).flatMapThrowing { throw SillyRangeError() } } 134 | return self.eventLoop.makeSucceededFuture(()) 135 | } 136 | 137 | XCTAssertThrowsError(try times2.wait()) 138 | XCTAssertLessThan(lock.withLock { value }, 6) 139 | } 140 | 141 | func testSequencedFlatMapEachCompact() throws { 142 | struct SillyRangeError: Error {} 143 | var last = "" 144 | let lock = NIOLock() 145 | let collection = self.eventLoop.makeSucceededFuture(["one", "2", "3", "not", "4", "1", "five", "^", "7"]) 146 | let times2 = collection.sequencedFlatMapEachCompact { val -> EventLoopFuture in 147 | guard let int = Int(val) else { return self.eventLoop.makeSucceededFuture(nil) } 148 | guard int < 4 else { return self.group.spinAll(on: self.eventLoop).flatMapThrowing { throw SillyRangeError() } } 149 | lock.withLock { last = val } 150 | return self.eventLoop.makeSucceededFuture(int * 2) 151 | } 152 | 153 | XCTAssertThrowsError(try times2.wait()) 154 | XCTAssertEqual(lock.withLock { last }, "3") 155 | } 156 | 157 | func testELSequencedFlatMapEach() throws { 158 | struct SillyRangeError: Error {} 159 | var value = 0 160 | let lock = NIOLock() 161 | let collection = [1, 2, 3, 4, 5, 6, 7, 8, 9] 162 | let times2 = collection.sequencedFlatMapEach(on: self.eventLoop) { int -> EventLoopFuture in 163 | lock.withLock { value = Swift.max(value, int) } 164 | guard int < 5 else { return self.group.spinAll(on: self.eventLoop).flatMapThrowing { throw SillyRangeError() } } 165 | return self.eventLoop.makeSucceededFuture(int * 2) 166 | } 167 | 168 | XCTAssertThrowsError(try times2.wait()) 169 | XCTAssertLessThan(lock.withLock { value }, 6) 170 | } 171 | 172 | func testELSequencedFlatMapVoid() throws { 173 | struct SillyRangeError: Error {} 174 | var value = 0 175 | let lock = NIOLock() 176 | let collection = [1, 2, 3, 4, 5, 6, 7, 8, 9] 177 | let times2 = collection.sequencedFlatMapEach(on: self.eventLoop) { int -> EventLoopFuture in 178 | lock.withLock { value = Swift.max(value, int) } 179 | guard int < 5 else { return self.group.spinAll(on: self.eventLoop).flatMapThrowing { throw SillyRangeError() } } 180 | return self.eventLoop.makeSucceededFuture(()) 181 | } 182 | 183 | XCTAssertThrowsError(try times2.wait()) 184 | XCTAssertLessThan(lock.withLock { value }, 6) 185 | } 186 | 187 | func testELSequencedFlatMapEachCompact() throws { 188 | struct SillyRangeError: Error {} 189 | var last = "" 190 | let lock = NIOLock() 191 | let collection = ["one", "2", "3", "not", "4", "1", "five", "^", "7"] 192 | let times2 = collection.sequencedFlatMapEachCompact(on: self.eventLoop) { val -> EventLoopFuture in 193 | guard let int = Int(val) else { return self.eventLoop.makeSucceededFuture(nil) } 194 | guard int < 4 else { return self.group.spinAll(on: self.eventLoop).flatMapThrowing { throw SillyRangeError() } } 195 | lock.withLock { last = val } 196 | return self.eventLoop.makeSucceededFuture(int * 2) 197 | } 198 | 199 | XCTAssertThrowsError(try times2.wait()) 200 | XCTAssertEqual(lock.withLock { last }, "3") 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/Optional+StrictMap.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | /// Given one or more optionals as inputs, checks whether each input is `nil`. If _any_ input is `nil`, `nil` is 4 | /// immediately returned as an overall results. If all of the inputs have values, the `transform` callback is invoked 5 | /// with all of the unwrapped values as parameters. 6 | /// 7 | /// - Note: This "baseline", single-item version of the function is trivially re-expressible using the `??` operator or 8 | /// `Optional.map(_:)`, but this is not the case for any of the other overloads. 9 | @inlinable 10 | public func strictMap( 11 | _ a: A?, 12 | _ transform: (A) throws -> Res 13 | ) rethrows -> Res? { 14 | guard let a = a else { return nil } 15 | return try transform(a) 16 | } 17 | 18 | /// `strictMap(_:_:)` over 2 optionals. 19 | @inlinable 20 | public func strictMap( 21 | _ a: A?, _ b: B?, 22 | _ transform: (A, B) throws -> Res 23 | ) rethrows -> Res? { 24 | guard let a = a, let b = b else { return nil } 25 | return try transform(a, b) 26 | } 27 | 28 | /// `strictMap(_:_:)` over 3 optionals. 29 | @inlinable 30 | public func strictMap( 31 | _ a: A?, _ b: B?, _ c: C?, 32 | _ transform: (A, B, C) throws -> Res 33 | ) rethrows -> Res? { 34 | guard let a = a, let b = b, let c = c else { return nil } 35 | return try transform(a, b, c) 36 | } 37 | 38 | /// `strictMap(_:_:)` over 4 optionals. 39 | @inlinable 40 | public func strictMap( 41 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, 42 | _ transform: (A, B, C, D) throws -> Res 43 | ) rethrows -> Res? { 44 | guard let a = a, let b = b, let c = c, let d = d else { return nil } 45 | return try transform(a, b, c, d) 46 | } 47 | 48 | /// `strictMap(_:_:)` over 5 optionals. 49 | @inlinable 50 | public func strictMap( 51 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, 52 | _ transform: (A, B, C, D, E) throws -> Res 53 | ) rethrows -> Res? { 54 | guard let a = a, let b = b, let c = c, let d = d, let e = e else { return nil } 55 | return try transform(a, b, c, d, e) 56 | } 57 | 58 | /// `strictMap(_:_:)` over 6 optionals. 59 | @inlinable 60 | public func strictMap( 61 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, 62 | _ transform: (A, B, C, D, E, F) throws -> Res 63 | ) rethrows -> Res? { 64 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f else { return nil } 65 | return try transform(a, b, c, d, e, f) 66 | } 67 | 68 | /// `strictMap(_:_:)` over 7 optionals. 69 | @inlinable 70 | public func strictMap( 71 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, 72 | _ transform: (A, B, C, D, E, F, G) throws -> Res 73 | ) rethrows -> Res? { 74 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g else { return nil } 75 | return try transform(a, b, c, d, e, f, g) 76 | } 77 | 78 | /// `strictMap(_:_:)` over 8 optionals. 79 | @inlinable 80 | public func strictMap( 81 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, 82 | _ transform: (A, B, C, D, E, F, G, H) throws -> Res 83 | ) rethrows -> Res? { 84 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h else { return nil } 85 | return try transform(a, b, c, d, e, f, g, h) 86 | } 87 | 88 | /// `strictMap(_:_:)` over 9 optionals. 89 | @inlinable 90 | public func strictMap( 91 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, 92 | _ transform: (A, B, C, D, E, F, G, H, I) throws -> Res 93 | ) rethrows -> Res? { 94 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i else { return nil } 95 | return try transform(a, b, c, d, e, f, g, h, i) 96 | } 97 | 98 | /// `strictMap(_:_:)` over 10 optionals. 99 | @inlinable 100 | public func strictMap( 101 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, 102 | _ transform: (A, B, C, D, E, F, G, H, I, J) throws -> Res 103 | ) rethrows -> Res? { 104 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j else { return nil } 105 | return try transform(a, b, c, d, e, f, g, h, i, j) 106 | } 107 | 108 | /// `strictMap(_:_:)` over 11 optionals. 109 | @inlinable 110 | public func strictMap( 111 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, 112 | _ transform: (A, B, C, D, E, F, G, H, I, J, K) throws -> Res 113 | ) rethrows -> Res? { 114 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k else { return nil } 115 | return try transform(a, b, c, d, e, f, g, h, i, j, k) 116 | } 117 | 118 | /// `strictMap(_:_:)` over 12 optionals. 119 | @inlinable 120 | public func strictMap( 121 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, _ l: L?, 122 | _ transform: (A, B, C, D, E, F, G, H, I, J, K, L) throws -> Res 123 | ) rethrows -> Res? { 124 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k, let l = l else { return nil } 125 | return try transform(a, b, c, d, e, f, g, h, i, j, k, l) 126 | } 127 | 128 | /// `strictMap(_:_:)` over 13 optionals. 129 | @inlinable 130 | public func strictMap( 131 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, _ l: L?, _ m: M?, 132 | _ transform: (A, B, C, D, E, F, G, H, I, J, K, L, M) throws -> Res 133 | ) rethrows -> Res? { 134 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k, let l = l, let m = m else { return nil } 135 | return try transform(a, b, c, d, e, f, g, h, i, j, k, l, m) 136 | } 137 | 138 | /// `strictMap(_:_:)` over 14 optionals. 139 | @inlinable 140 | public func strictMap( 141 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, _ l: L?, _ m: M?, _ n: N?, 142 | _ transform: (A, B, C, D, E, F, G, H, I, J, K, L, M, N) throws -> Res 143 | ) rethrows -> Res? { 144 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k, let l = l, let m = m, let n = n else { return nil } 145 | return try transform(a, b, c, d, e, f, g, h, i, j, k, l, m, n) 146 | } 147 | 148 | /// `strictMap(_:_:)` over 15 optionals. 149 | @inlinable 150 | public func strictMap( 151 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, _ l: L?, _ m: M?, _ n: N?, _ o: O?, 152 | _ transform: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) throws -> Res 153 | ) rethrows -> Res? { 154 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k, let l = l, let m = m, let n = n, let o = o else { return nil } 155 | return try transform(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) 156 | } 157 | 158 | /// `strictMap(_:_:)` over 16 optionals. 159 | @inlinable 160 | public func strictMap( 161 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, _ l: L?, _ m: M?, _ n: N?, _ o: O?, _ p: P?, 162 | _ transform: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) throws -> Res 163 | ) rethrows -> Res? { 164 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k, let l = l, let m = m, let n = n, let o = o, let p = p else { return nil } 165 | return try transform(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) 166 | } 167 | 168 | /// `strictMap(_:_:)` over 17 optionals. 169 | @inlinable 170 | public func strictMap( 171 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, _ l: L?, _ m: M?, _ n: N?, _ o: O?, _ p: P?, _ q: Q?, 172 | _ transform: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) throws -> Res 173 | ) rethrows -> Res? { 174 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k, let l = l, let m = m, let n = n, let o = o, let p = p, let q = q else { return nil } 175 | return try transform(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) 176 | } 177 | 178 | /// `strictMap(_:_:)` over 18 optionals. 179 | @inlinable 180 | public func strictMap( 181 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, _ l: L?, _ m: M?, _ n: N?, _ o: O?, _ p: P?, _ q: Q?, _ r: R?, 182 | _ transform: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) throws -> Res 183 | ) rethrows -> Res? { 184 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k, let l = l, let m = m, let n = n, let o = o, let p = p, let q = q, let r = r else { return nil } 185 | return try transform(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) 186 | } 187 | 188 | /// `strictMap(_:_:)` over 19 optionals. 189 | @inlinable 190 | public func strictMap( 191 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, _ l: L?, _ m: M?, _ n: N?, _ o: O?, _ p: P?, _ q: Q?, _ r: R?, _ s: S?, 192 | _ transform: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) throws -> Res 193 | ) rethrows -> Res? { 194 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k, let l = l, let m = m, let n = n, let o = o, let p = p, let q = q, let r = r, let s = s else { return nil } 195 | return try transform(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) 196 | } 197 | 198 | /// `strictMap(_:_:)` over 20 optionals. 199 | @inlinable 200 | public func strictMap( 201 | _ a: A?, _ b: B?, _ c: C?, _ d: D?, _ e: E?, _ f: F?, _ g: G?, _ h: H?, _ i: I?, _ j: J?, _ k: K?, _ l: L?, _ m: M?, _ n: N?, _ o: O?, _ p: P?, _ q: Q?, _ r: R?, _ s: S?, _ t: T?, 202 | _ transform: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) throws -> Res 203 | ) rethrows -> Res? { 204 | guard let a = a, let b = b, let c = c, let d = d, let e = e, let f = f, let g = g, let h = h, let i = i, let j = j, let k = k, let l = l, let m = m, let n = n, let o = o, let p = p, let q = q, let r = r, let s = s, let t = t else { return nil } 205 | return try transform(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) 206 | } 207 | -------------------------------------------------------------------------------- /Sources/AsyncKit/EventLoopFuture/Future+Collection.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension EventLoopFuture where Value: Sequence { 4 | /// Calls a closure on each element in the sequence that is wrapped by an `EventLoopFuture`. 5 | /// 6 | /// let collection = eventLoop.future([1, 2, 3, 4, 5, 6, 7, 8, 9]) 7 | /// let times2 = collection.mapEach { int in 8 | /// return int * 2 9 | /// } 10 | /// // times2: EventLoopFuture([2, 4, 6, 8, 10, 12, 14, 16, 18]) 11 | /// 12 | /// - parameters: 13 | /// - transform: The closure that each element in the sequence is passed into. 14 | /// - element: The element from the sequence that you can operate on. 15 | /// - returns: A new `EventLoopFuture` that wraps the sequence of transformed elements. 16 | public func mapEach( 17 | _ transform: @escaping (_ element: Value.Element) -> Result 18 | ) -> EventLoopFuture<[Result]> { 19 | return self.map { $0.map(transform) } 20 | } 21 | 22 | /// Gets the value of a key path for each element in the sequence that is wrapped by an `EventLoopFuture`. 23 | /// 24 | /// let collection = eventLoop.future(["a", "bb", "ccc", "dddd", "eeeee"]) 25 | /// let lengths = collection.mapEach(\.count) 26 | /// // lengths: EventLoopFuture([1, 2, 3, 4, 5]) 27 | /// 28 | /// - parameters: 29 | /// - keyPath: The key path to access on each element in the sequence. 30 | /// - returns: A new `EventLoopFuture` that wraps the sequence of key path values. 31 | public func mapEach( 32 | _ keyPath: KeyPath 33 | ) -> EventLoopFuture<[Result]> { 34 | return self.map { $0.map { $0[keyPath: keyPath] } } 35 | } 36 | 37 | /// Calls a closure, which returns an `Optional`, on each element in the sequence that is wrapped by an `EventLoopFuture`. 38 | /// 39 | /// let collection = eventLoop.future(["one", "2", "3", "4", "five", "^", "7"]) 40 | /// let times2 = collection.mapEachCompact { int in 41 | /// return Int(int) 42 | /// } 43 | /// // times2: EventLoopFuture([2, 3, 4, 7]) 44 | /// 45 | /// - parameters: 46 | /// - transform: The closure that each element in the sequence is passed into. 47 | /// - element: The element from the sequence that you can operate on. 48 | /// - returns: A new `EventLoopFuture` that wraps the sequence of transformed elements. 49 | public func mapEachCompact( 50 | _ transform: @escaping (_ element: Value.Element) -> Result? 51 | ) -> EventLoopFuture<[Result]> { 52 | return self.map { $0.compactMap(transform) } 53 | } 54 | 55 | /// Gets the optional value of a key path for each element in the sequence that is wrapped by an `EventLoopFuture`. 56 | /// 57 | /// let collection = eventLoop.future(["asdf", "qwer", "zxcv", ""]) 58 | /// let letters = collection.mapEachCompact(\.first) 59 | /// // letters: EventLoopFuture(["a", "q", "z"]) 60 | /// 61 | /// - parameters: 62 | /// - keyPath: The key path to access on each element in the sequence. 63 | /// - returns: A new `EventLoopFuture` that wraps the sequence of non-nil key path values. 64 | public func mapEachCompact( 65 | _ keyPath: KeyPath 66 | ) -> EventLoopFuture<[Result]> { 67 | return self.map { $0.compactMap { $0[keyPath: keyPath] } } 68 | } 69 | 70 | /// Calls a closure which returns a collection on each element in the sequence that is wrapped by an `EventLoopFuture`, 71 | /// combining the results into a single result collection. 72 | /// 73 | /// let collection = eventLoop.future([[1, 2, 3], [9, 8, 7], [], [0]]) 74 | /// let flat = collection.mapEachFlat { $0 } 75 | /// // flat: [1, 2, 3, 9, 8, 7, 0] 76 | /// 77 | /// - parameters: 78 | /// - transform: The closure that each element in the sequence is passed into. 79 | /// - element: The element from the sequence that you can operate on. 80 | /// - returns: A new `EventLoopFuture` that wraps the flattened sequence of transformed elements. 81 | public func mapEachFlat( 82 | _ transform: @escaping (_ element: Value.Element) -> ResultSegment 83 | ) -> EventLoopFuture<[ResultSegment.Element]> { 84 | return self.map { $0.flatMap(transform) } 85 | } 86 | 87 | /// Gets the collection value of a key path for each element in the sequence that is wrapped by an `EventLoopFuture`, 88 | /// combining the results into a single result collection. 89 | /// 90 | /// let collection = eventLoop.future(["ABC", "👩‍👩‍👧‍👧"]) 91 | /// let flat = collection.mapEachFlat(\.utf8CString) 92 | /// // flat: [65, 66, 67, 0, -16, -97, -111, -87, -30, -128, -115, -16, -97, -111, -87, -30, 93 | /// // -128, -115, -16, -97, -111, -89, -30, -128, -115, -16, -97, -111, -89, 0] 94 | /// 95 | /// - parameters: 96 | /// - keyPath: The key path to access on each element in the sequence. 97 | /// - returns: A new `EventLoopFuture` that wraps the flattened sequence of transformed elements. 98 | public func mapEachFlat( 99 | _ keyPath: KeyPath 100 | ) -> EventLoopFuture<[ResultSegment.Element]> { 101 | return self.map { $0.flatMap { $0[keyPath: keyPath] } } 102 | } 103 | 104 | /// Calls a closure, which returns an `EventLoopFuture`, on each element 105 | /// in a sequence that is wrapped by an `EventLoopFuture`. 106 | /// 107 | /// let users = eventLoop.future([User(name: "Tanner", ...), ...]) 108 | /// let saved = users.flatMapEach(on: eventLoop) { $0.save(on: database) } 109 | /// 110 | /// - parameters: 111 | /// - eventLoop: The `EventLoop` to flatten the resulting array of futures on. 112 | /// - transform: The closure that each element in the sequence is passed into. 113 | /// - element: The element from the sequence that you can operate on. 114 | /// - returns: A new `EventLoopFuture` that wraps the results 115 | /// of all the `EventLoopFuture`s returned from the closure. 116 | public func flatMapEach( 117 | on eventLoop: any EventLoop, 118 | _ transform: @escaping (_ element: Value.Element) -> EventLoopFuture 119 | ) -> EventLoopFuture<[Result]> { 120 | self.flatMap { .reduce(into: [], $0.map(transform), on: eventLoop) { $0.append($1) } } 121 | } 122 | 123 | /// Calls a closure, which returns an `EventLoopFuture`, on each element 124 | /// in a sequence that is wrapped by an `EventLoopFuture`. No results from 125 | /// each future are expected. 126 | /// 127 | /// let users = eventLoop.future([User(name: "Tanner", ...), ...]) 128 | /// let saved = users.flatMapEach(on: eventLoop) { $0.save(on: database) } 129 | /// 130 | /// - parameters: 131 | /// - eventLoop: The `EventLoop` to flatten the resulting array of futures on. 132 | /// - transform: The closure that each element in the sequence is passed into. 133 | /// - element: The element from the sequence that you can operate on. 134 | /// - returns: A new `EventLoopFuture` that completes when all the returned 135 | /// `EVentLoopFuture`s do. 136 | public func flatMapEach( 137 | on eventLoop: any EventLoop, 138 | _ transform: @escaping (_ element: Value.Element) -> EventLoopFuture 139 | ) -> EventLoopFuture { 140 | self.flatMap { .andAllSucceed($0.map(transform), on: eventLoop) } 141 | } 142 | 143 | /// Calls a closure, which returns an `EventLoopFuture`, on each element 144 | /// in a sequence that is wrapped by an `EventLoopFuture`. 145 | /// 146 | /// let users = eventLoop.future([User(name: "Tanner", ...), ...]) 147 | /// let pets = users.flatMapEach(on: eventLoop) { $0.favoritePet(on: database) } 148 | /// 149 | /// - parameters: 150 | /// - eventLoop: The `EventLoop` to flatten the resulting array of futures on. 151 | /// - transform: The closure that each element in the sequence is passed into. 152 | /// - element: The element from the sequence that you can operate on. 153 | /// - returns: A new `EventLoopFuture` that wraps the non-nil results 154 | /// of all the `EventLoopFuture`s returned from the closure. 155 | public func flatMapEachCompact( 156 | on eventLoop: any EventLoop, 157 | _ transform: @escaping (_ element: Value.Element) -> EventLoopFuture 158 | ) -> EventLoopFuture<[Result]> { 159 | self.flatMap { .reduce(into: [], $0.map(transform), on: eventLoop) { res, elem in elem.map { res.append($0) } } } 160 | } 161 | 162 | /// Calls a closure on each element in the sequence that is wrapped by an `EventLoopFuture`. 163 | /// 164 | /// let collection = eventLoop.future([1, 2, 3, 4, 5, 6, 7, 8, 9]) 165 | /// let times2 = collection.flatMapEachThrowing { int in 166 | /// guard int < 10 else { throw RangeError.oops } 167 | /// return int * 2 168 | /// } 169 | /// // times2: EventLoopFuture([2, 4, 6, 8, 10, 12, 14, 16, 18]) 170 | /// 171 | /// If your callback function throws, the returned `EventLoopFuture` will error. 172 | /// 173 | /// - parameters: 174 | /// - transform: The closure that each element in the sequence is passed into. 175 | /// - element: The element from the sequence that you can operate on. 176 | /// - returns: A new `EventLoopFuture` that wraps the sequence of transformed elements. 177 | public func flatMapEachThrowing( 178 | _ transform: @escaping (_ element: Value.Element) throws -> Result 179 | ) -> EventLoopFuture<[Result]> { 180 | return self.flatMapThrowing { sequence -> [Result] in 181 | return try sequence.map(transform) 182 | } 183 | } 184 | 185 | /// Calls a closure, which returns an `Optional`, on each element in the sequence that is wrapped by an `EventLoopFuture`. 186 | /// 187 | /// let collection = eventLoop.future(["one", "2", "3", "4", "five", "^", "7"]) 188 | /// let times2 = collection.mapEachCompact { int in 189 | /// return Int(int) 190 | /// } 191 | /// // times2: EventLoopFuture([2, 3, 4, 7]) 192 | /// 193 | /// If your callback function throws, the returned `EventLoopFuture` will error. 194 | /// 195 | /// - parameters: 196 | /// - transform: The closure that each element in the sequence is passed into. 197 | /// - element: The element from the sequence that you can operate on. 198 | /// - returns: A new `EventLoopFuture` that wraps the sequence of transformed elements. 199 | public func flatMapEachCompactThrowing( 200 | _ transform: @escaping (_ element: Value.Element) throws -> Result? 201 | ) -> EventLoopFuture<[Result]> { 202 | return self.flatMapThrowing { sequence -> [Result] in 203 | return try sequence.compactMap(transform) 204 | } 205 | } 206 | 207 | /// A variant form of `flatMapEach(on:_:)` which guarantees: 208 | /// 209 | /// 1) Explicitly sequential execution of each future returned by the mapping 210 | /// closure; the next future does not being executing until the previous one 211 | /// has yielded a success result. 212 | /// 213 | /// 2) No further futures will be even partially executed if any one future 214 | /// returns a failure result. 215 | /// 216 | /// Neither of these are provided by the original version of the method. 217 | public func sequencedFlatMapEach(_ transform: @escaping (_ element: Value.Element) -> EventLoopFuture) -> EventLoopFuture<[Result]> { 218 | var results: [Result] = [] 219 | 220 | return self.flatMap { 221 | $0.reduce(self.eventLoop.future()) { fut, elem in 222 | fut.flatMap { transform(elem).map { results.append($0) } } 223 | } 224 | }.transform(to: results) 225 | } 226 | 227 | /// An overload of `sequencedFlatMapEach(_:)` which returns a `Void` future instead 228 | /// of `[Void]` when the result type of the transform closure is `Void`. 229 | public func sequencedFlatMapEach(_ transform: @escaping (_ element: Value.Element) -> EventLoopFuture) -> EventLoopFuture { 230 | return self.flatMap { 231 | $0.reduce(self.eventLoop.future()) { fut, elem in 232 | fut.flatMap { transform(elem) } 233 | } 234 | } 235 | } 236 | 237 | /// Variant of `sequencedFlatMapEach(_:)` which provides `compactMap()` semantics 238 | /// by allowing result values to be `nil`. Such results are not included in the 239 | /// output array. 240 | public func sequencedFlatMapEachCompact(_ transform: @escaping (_ element: Value.Element) -> EventLoopFuture) -> EventLoopFuture<[Result]> { 241 | var results: [Result] = [] 242 | 243 | return self.flatMap { 244 | $0.reduce(self.eventLoop.future()) { fut, elem in 245 | fut.flatMap { transform(elem).map { 246 | $0.map { results.append($0) } 247 | } } 248 | } 249 | }.transform(to: results) 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/Future+ConjunctionTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncKit 2 | import XCTest 3 | import NIOCore 4 | 5 | final class FutureConjunctionTests: AsyncKitTestCase { 6 | func testTrivialStrictMapCorrectness() throws { 7 | XCTAssertNotNil(strictMap(()) { $0 }) 8 | XCTAssertNil(strictMap(Void?.none) { _ in () }) 9 | 10 | XCTAssertNotNil(strictMap((), ()) { $1 }) 11 | XCTAssertNil(strictMap(Void?.none, ()) { $1 }) 12 | XCTAssertNil(strictMap((), Void?.none) { $1 }) 13 | XCTAssertNil(strictMap(Void?.none, Void?.none) { $1 }) 14 | 15 | XCTAssertNotNil(strictMap((), (), ()) { $2 }) 16 | XCTAssertNil(strictMap(Void?.none, (), ()) { $2 }) 17 | XCTAssertNil(strictMap((), Void?.none, ()) { $2 }) 18 | XCTAssertNil(strictMap((), (), Void?.none) { $2 }) 19 | XCTAssertNil(strictMap(Void?.none, Void?.none, ()) { $2 }) 20 | XCTAssertNil(strictMap(Void?.none, (), Void?.none) { $2 }) 21 | XCTAssertNil(strictMap((), Void?.none, Void?.none) { $2 }) 22 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none) { $2 }) 23 | 24 | XCTAssertNotNil(strictMap((), (), (), ()) { $3 }) 25 | XCTAssertNil(strictMap(Void?.none, (), (), ()) { $3 }) 26 | XCTAssertNil(strictMap((), Void?.none, (), ()) { $3 }) 27 | XCTAssertNil(strictMap((), (), Void?.none, ()) { $3 }) 28 | XCTAssertNil(strictMap((), (), (), Void?.none) { $3 }) 29 | XCTAssertNil(strictMap(Void?.none, Void?.none, (), ()) { $3 }) 30 | XCTAssertNil(strictMap(Void?.none, (), Void?.none, ()) { $3 }) 31 | XCTAssertNil(strictMap(Void?.none, (), (), Void?.none) { $3 }) 32 | XCTAssertNil(strictMap((), Void?.none, Void?.none, ()) { $3 }) 33 | XCTAssertNil(strictMap((), Void?.none, (), Void?.none) { $3 }) 34 | XCTAssertNil(strictMap((), (), Void?.none, Void?.none) { $3 }) 35 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, ()) { $3 }) 36 | XCTAssertNil(strictMap(Void?.none, Void?.none, (), Void?.none) { $3 }) 37 | XCTAssertNil(strictMap(Void?.none, (), Void?.none, Void?.none) { $3 }) 38 | XCTAssertNil(strictMap((), Void?.none, Void?.none, Void?.none) { $3 }) 39 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none) { $3 }) 40 | 41 | // The full test set gets absurd after more or less this point so just do the minimums to satisfy test coverage 42 | XCTAssertNotNil(strictMap((), (), (), (), ()) { $4 }) 43 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $4 }) 44 | XCTAssertNotNil(strictMap((), (), (), (), (), ()) { $5 }) 45 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $5 }) 46 | XCTAssertNotNil(strictMap((), (), (), (), (), (), ()) { $6 }) 47 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $6 }) 48 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), ()) { $7 }) 49 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $7 }) 50 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), ()) { $8 }) 51 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $8 }) 52 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), ()) { $9 }) 53 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $9 }) 54 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), ()) { $10 }) 55 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $10 }) 56 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), (), ()) { $11 }) 57 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $11 }) 58 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), (), (), ()) { $12 }) 59 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $12 }) 60 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), (), (), (), ()) { $13 }) 61 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $13 }) 62 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), (), (), (), (), ()) { $14 }) 63 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $14 }) 64 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), (), (), (), (), (), ()) { $15 }) 65 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $15 }) 66 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), ()) { $16 }) 67 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $16 }) 68 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), ()) { $17 }) 69 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $17 }) 70 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), ()) { $18 }) 71 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $18 }) 72 | XCTAssertNotNil(strictMap((), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), (), ()) { $19 }) 73 | XCTAssertNil(strictMap(Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none, Void?.none) { $19 }) 74 | } 75 | 76 | func testTrivialWhenTheySucceedCorectness() throws { 77 | let el1 = self.group.any() 78 | let el2 = self.group.any() 79 | 80 | let f1 = el1.submit { return "string value" } 81 | let f2 = el1.submit { return Int.min } 82 | let f3 = el2.submit { return true } 83 | 84 | let result = try EventLoopFuture.whenTheySucceed(f1, f2, f3).wait() 85 | 86 | XCTAssertEqual(result.0, "string value") 87 | XCTAssertEqual(result.1, Int.min) 88 | XCTAssertEqual(result.2, true) 89 | } 90 | 91 | func testInvokingAllVariantsOfWhenTheySucceed() throws { 92 | // Yes, this test is using futures that are all the same type and thus could be passed to 93 | // `whenAllSucceed()` instead of `whenTheySucceed()`. Doesn't matter - the point in this 94 | // test is to cover all the variants for minimum correctness, not test full functionality. 95 | do { 96 | let f = (0 ..< 2).map { n in self.eventLoop.submit { n } } 97 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1]).wait() 98 | XCTAssert(r.0 == 0 && r.1 == 1) 99 | } 100 | do { 101 | let f = (0 ..< 3).map { n in self.eventLoop.submit { n } } 102 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2]).wait() 103 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2) 104 | } 105 | do { 106 | let f = (0 ..< 4).map { n in self.eventLoop.submit { n } } 107 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3]).wait() 108 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3) 109 | } 110 | do { 111 | let f = (0 ..< 5).map { n in self.eventLoop.submit { n } } 112 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4]).wait() 113 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4) 114 | } 115 | do { 116 | let f = (0 ..< 6).map { n in self.eventLoop.submit { n } } 117 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5]).wait() 118 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5) 119 | } 120 | do { 121 | let f = (0 ..< 7).map { n in self.eventLoop.submit { n } } 122 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6]).wait() 123 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6) 124 | } 125 | do { 126 | let f = (0 ..< 8).map { n in self.eventLoop.submit { n } } 127 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7]).wait() 128 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7) 129 | } 130 | do { 131 | let f = (0 ..< 9).map { n in self.eventLoop.submit { n } } 132 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]).wait() 133 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8) 134 | } 135 | do { 136 | let f = (0 ..< 10).map { n in self.eventLoop.submit { n } } 137 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9]).wait() 138 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9) 139 | } 140 | do { 141 | let f = (0 ..< 11).map { n in self.eventLoop.submit { n } } 142 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10]).wait() 143 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10) 144 | } 145 | do { 146 | let f = (0 ..< 12).map { n in self.eventLoop.submit { n } } 147 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11]).wait() 148 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10 && r.11 == 11) 149 | } 150 | do { 151 | let f = (0 ..< 13).map { n in self.eventLoop.submit { n } } 152 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11], f[12]).wait() 153 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10 && r.11 == 11 && r.12 == 12) 154 | } 155 | do { 156 | let f = (0 ..< 14).map { n in self.eventLoop.submit { n } } 157 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11], f[12], f[13]).wait() 158 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10 && r.11 == 11 && r.12 == 12 && r.13 == 13) 159 | } 160 | do { 161 | let f = (0 ..< 15).map { n in self.eventLoop.submit { n } } 162 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11], f[12], f[13], f[14]).wait() 163 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10 && r.11 == 11 && r.12 == 12 && r.13 == 13 && r.14 == 14) 164 | } 165 | do { 166 | let f = (0 ..< 16).map { n in self.eventLoop.submit { n } } 167 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11], f[12], f[13], f[14], f[15]).wait() 168 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10 && r.11 == 11 && r.12 == 12 && r.13 == 13 && r.14 == 14 && r.15 == 15) 169 | } 170 | do { 171 | let f = (0 ..< 17).map { n in self.eventLoop.submit { n } } 172 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11], f[12], f[13], f[14], f[15], f[16]).wait() 173 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10 && r.11 == 11 && r.12 == 12 && r.13 == 13 && r.14 == 14 && r.15 == 15 && r.16 == 16) 174 | } 175 | do { 176 | let f = (0 ..< 18).map { n in self.eventLoop.submit { n } } 177 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11], f[12], f[13], f[14], f[15], f[16], f[17]).wait() 178 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10 && r.11 == 11 && r.12 == 12 && r.13 == 13 && r.14 == 14 && r.15 == 15 && r.16 == 16 && r.17 == 17) 179 | } 180 | do { 181 | let f = (0 ..< 19).map { n in self.eventLoop.submit { n } } 182 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11], f[12], f[13], f[14], f[15], f[16], f[17], f[18]).wait() 183 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10 && r.11 == 11 && r.12 == 12 && r.13 == 13 && r.14 == 14 && r.15 == 15 && r.16 == 16 && r.17 == 17 && r.18 == 18) 184 | } 185 | do { 186 | let f = (0 ..< 20).map { n in self.eventLoop.submit { n } } 187 | let r = try EventLoopFuture.whenTheySucceed(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9], f[10], f[11], f[12], f[13], f[14], f[15], f[16], f[17], f[18], f[19]).wait() 188 | XCTAssert(r.0 == 0 && r.1 == 1 && r.2 == 2 && r.3 == 3 && r.4 == 4 && r.5 == 5 && r.6 == 6 && r.7 == 7 && r.8 == 8 && r.9 == 9 && r.10 == 10 && r.11 == 11 && r.12 == 12 && r.13 == 13 && r.14 == 14 && r.15 == 15 && r.16 == 16 && r.17 == 17 && r.18 == 18 && r.19 == 19) 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Tests/AsyncKitTests/ConnectionPoolTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AsyncKit 2 | import Atomics 3 | import Logging 4 | import NIOConcurrencyHelpers 5 | import NIOCore 6 | import NIOEmbedded 7 | import XCTest 8 | 9 | final class ConnectionPoolTests: AsyncKitTestCase { 10 | func testPooling() throws { 11 | let foo = FooDatabase() 12 | let pool = EventLoopConnectionPool( 13 | source: foo, 14 | maxConnections: 2, 15 | on: self.group.any() 16 | ) 17 | defer { try! pool.close().wait() } 18 | 19 | // make two connections 20 | let connA = try pool.requestConnection().wait() 21 | XCTAssertEqual(connA.isClosed, false) 22 | let connB = try pool.requestConnection().wait() 23 | XCTAssertEqual(connB.isClosed, false) 24 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 2) 25 | 26 | // try to make a third, but pool only supports 2 27 | let futureC = pool.requestConnection() 28 | let connC = ManagedAtomic(nil) 29 | futureC.whenSuccess { connC.store($0, ordering: .relaxed) } 30 | XCTAssertNil(connC.load(ordering: .relaxed)) 31 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 2) 32 | 33 | // release one of the connections, allowing the third to be made 34 | pool.releaseConnection(connB) 35 | let connCRet = try futureC.wait() 36 | XCTAssertNotNil(connC.load(ordering: .relaxed)) 37 | XCTAssert(connC.load(ordering: .relaxed) === connB) 38 | XCTAssert(connCRet === connC.load(ordering: .relaxed)) 39 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 2) 40 | 41 | // try to make a third again, with two active 42 | let futureD = pool.requestConnection() 43 | let connD = ManagedAtomic(nil) 44 | futureD.whenSuccess { connD.store($0, ordering: .relaxed) } 45 | XCTAssertNil(connD.load(ordering: .relaxed)) 46 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 2) 47 | 48 | // this time, close the connection before releasing it 49 | try connCRet.close().wait() 50 | pool.releaseConnection(connC.load(ordering: .relaxed)!) 51 | let connDRet = try futureD.wait() 52 | XCTAssert(connD.load(ordering: .relaxed) !== connB) 53 | XCTAssert(connDRet === connD.load(ordering: .relaxed)) 54 | XCTAssertEqual(connD.load(ordering: .relaxed)?.isClosed, false) 55 | XCTAssertEqual(foo.connectionsCreated.load(ordering: .relaxed), 3) 56 | } 57 | 58 | func testConnectionPruning() throws { 59 | let foo = FooDatabase() 60 | let pool = EventLoopConnectionPool( 61 | source: foo, 62 | maxConnections: 5, 63 | pruneInterval: .milliseconds(200), 64 | maxIdleTimeBeforePruning: .milliseconds(300), 65 | on: MultiThreadedEventLoopGroup(numberOfThreads: 1).next() 66 | ) 67 | 68 | defer { try! pool.close().wait() } 69 | 70 | let connA = try pool.requestConnection().wait() 71 | 72 | let anotherConnection1 = try pool.requestConnection().wait() 73 | let anotherConnection2 = try pool.requestConnection().wait() 74 | 75 | pool.releaseConnection(connA) 76 | 77 | let connA1 = try pool.requestConnection().wait() 78 | XCTAssert(connA === connA1) 79 | pool.releaseConnection(connA1) 80 | 81 | // Keeping connection alive by using it and closing it 82 | for _ in 0..<3 { 83 | Thread.sleep(forTimeInterval: 0.2) 84 | let connA2 = try pool.requestConnection().wait() 85 | XCTAssert(connA === connA2) 86 | pool.releaseConnection(connA2) 87 | } 88 | 89 | pool.releaseConnection(anotherConnection1) 90 | pool.releaseConnection(anotherConnection2) 91 | 92 | Thread.sleep(forTimeInterval: 0.7) 93 | let (knownConnections, activeConnections, openConnections) = try pool.poolState().wait() 94 | XCTAssertEqual(knownConnections, 0) 95 | XCTAssertEqual(activeConnections, 0) 96 | XCTAssertEqual(openConnections, 0) 97 | 98 | let connB = try pool.requestConnection().wait() 99 | XCTAssert(connA !== connB) 100 | let newActiveConnections = try pool.poolState().wait().active 101 | XCTAssertEqual(newActiveConnections, 1) 102 | } 103 | 104 | func testFIFOWaiters() throws { 105 | let foo = FooDatabase() 106 | let pool = EventLoopConnectionPool( 107 | source: foo, 108 | maxConnections: 1, 109 | on: self.group.any() 110 | ) 111 | defer { try! pool.close().wait() } 112 | 113 | // * User A makes a request for a connection, gets connection number 1. 114 | let a_1 = pool.requestConnection() 115 | let a = try a_1.wait() 116 | 117 | // * User B makes a request for a connection, they are exhausted so he gets a promise. 118 | let b_1 = pool.requestConnection() 119 | 120 | // * User A makes another request for a connection, they are still exhausted so he gets a promise. 121 | let a_2 = pool.requestConnection() 122 | 123 | // * User A returns connection number 1. His previous request is fulfilled with connection number 1. 124 | pool.releaseConnection(a) 125 | 126 | // * User B gets his connection 127 | let b = try b_1.wait() 128 | XCTAssert(a === b) 129 | 130 | // * User B releases his connection 131 | pool.releaseConnection(b) 132 | 133 | // * User A's second connection request is fulfilled 134 | let c = try a_2.wait() 135 | XCTAssert(a === c) 136 | } 137 | 138 | func testConnectError() throws { 139 | let db = ErrorDatabase() 140 | let pool = EventLoopConnectionPool( 141 | source: db, 142 | maxConnections: 1, 143 | on: self.group.any() 144 | ) 145 | defer { try! pool.close().wait() } 146 | 147 | do { 148 | _ = try pool.requestConnection().wait() 149 | XCTFail("should not have created connection") 150 | } catch _ as ErrorDatabase.Error { 151 | // pass 152 | } 153 | 154 | // test that we can still make another request even after a failed request 155 | do { 156 | _ = try pool.requestConnection().wait() 157 | XCTFail("should not have created connection") 158 | } catch _ as ErrorDatabase.Error { 159 | // pass 160 | } 161 | } 162 | 163 | func testPoolClose() throws { 164 | let foo = FooDatabase() 165 | let pool = EventLoopConnectionPool( 166 | source: foo, 167 | maxConnections: 1, 168 | on: self.group.any() 169 | ) 170 | let _ = try pool.requestConnection().wait() 171 | let b = pool.requestConnection() 172 | try pool.close().wait() 173 | 174 | let c = pool.requestConnection() 175 | 176 | // check that waiters are failed 177 | do { 178 | _ = try b.wait() 179 | XCTFail("should not have created connection") 180 | } catch ConnectionPoolError.shutdown { 181 | // pass 182 | } 183 | 184 | // check that new requests fail 185 | do { 186 | _ = try c.wait() 187 | XCTFail("should not have created connection") 188 | } catch ConnectionPoolError.shutdown { 189 | // pass 190 | } 191 | } 192 | 193 | // https://github.com/vapor/async-kit/issues/63 194 | func testDeadlock() { 195 | let foo = FooDatabase() 196 | let pool = EventLoopConnectionPool( 197 | source: foo, 198 | maxConnections: 1, 199 | requestTimeout: .milliseconds(100), 200 | on: self.group.any() 201 | ) 202 | defer { try! pool.close().wait() } 203 | _ = pool.requestConnection() 204 | let start = Date() 205 | let a = pool.requestConnection() 206 | XCTAssertThrowsError(try a.wait(), "Connection should have deadlocked and thrown ConnectionPoolTimeoutError.connectionRequestTimeout") { (error) in 207 | let interval = Date().timeIntervalSince(start) 208 | XCTAssertGreaterThan(interval, 0.1) 209 | XCTAssertLessThan(interval, 0.25) 210 | XCTAssertEqual(error as? ConnectionPoolTimeoutError, ConnectionPoolTimeoutError.connectionRequestTimeout) 211 | } 212 | } 213 | 214 | /*func testPerformance() { 215 | guard performance(expected: 0.088) else { return } 216 | let foo = FooDatabase() 217 | let pool = EventLoopConnectionPool( 218 | source: foo, 219 | maxConnections: 3, 220 | on: self.group.any() 221 | ) 222 | 223 | measure { 224 | for _ in 0..<10_000 { 225 | do { 226 | let connA = try! pool.requestConnection().wait() 227 | pool.releaseConnection(connA) 228 | } 229 | do { 230 | let connA = try! pool.requestConnection().wait() 231 | let connB = try! pool.requestConnection().wait() 232 | let connC = try! pool.requestConnection().wait() 233 | pool.releaseConnection(connB) 234 | pool.releaseConnection(connC) 235 | pool.releaseConnection(connA) 236 | } 237 | do { 238 | let connA = try! pool.requestConnection().wait() 239 | let connB = try! pool.requestConnection().wait() 240 | pool.releaseConnection(connA) 241 | pool.releaseConnection(connB) 242 | } 243 | } 244 | } 245 | }*/ 246 | 247 | func testThreadSafety() throws { 248 | let foo = FooDatabase() 249 | let pool = EventLoopGroupConnectionPool( 250 | source: foo, 251 | maxConnectionsPerEventLoop: 2, 252 | on: self.group 253 | ) 254 | defer { pool.shutdown() } 255 | 256 | var futures: [EventLoopFuture] = [] 257 | 258 | var eventLoops = group.makeIterator() 259 | while let eventLoop = eventLoops.next() { 260 | let promise = eventLoop.makePromise(of: Void.self) 261 | eventLoop.execute { 262 | (0 ..< 1_000).map { i in 263 | return pool.withConnection(on: eventLoop) { conn in 264 | return conn.eventLoop.makeSucceededFuture(i) 265 | } 266 | }.flatten(on: eventLoop) 267 | .map { XCTAssertEqual($0.count, 1_000) } 268 | .cascade(to: promise) 269 | } 270 | futures.append(promise.futureResult) 271 | } 272 | 273 | try futures.flatten(on: group.any()).wait() 274 | } 275 | 276 | func testGracefulShutdownAsync() throws { 277 | let foo = FooDatabase() 278 | let pool = EventLoopGroupConnectionPool( 279 | source: foo, 280 | maxConnectionsPerEventLoop: 2, 281 | on: self.group 282 | ) 283 | 284 | let expectation1 = XCTestExpectation(description: "Shutdown completion") 285 | let expectation2 = XCTestExpectation(description: "Shutdown completion with error") 286 | 287 | pool.shutdownGracefully { 288 | XCTAssertNil($0) 289 | expectation1.fulfill() 290 | } 291 | XCTWaiter().wait(for: [expectation1], timeout: 5.0) 292 | 293 | pool.shutdownGracefully { 294 | XCTAssertEqual($0 as? ConnectionPoolError, ConnectionPoolError.shutdown) 295 | expectation2.fulfill() 296 | } 297 | XCTWaiter().wait(for: [expectation2], timeout: 5.0) 298 | } 299 | 300 | func testGracefulShutdownSync() throws { 301 | let foo = FooDatabase() 302 | let pool = EventLoopGroupConnectionPool( 303 | source: foo, 304 | maxConnectionsPerEventLoop: 2, 305 | on: self.group 306 | ) 307 | 308 | XCTAssertNoThrow(try pool.syncShutdownGracefully()) 309 | XCTAssertThrowsError(try pool.syncShutdownGracefully()) { 310 | XCTAssertEqual($0 as? ConnectionPoolError, ConnectionPoolError.shutdown) 311 | } 312 | } 313 | 314 | func testGracefulShutdownWithHeldConnection() throws { 315 | let foo = FooDatabase() 316 | let pool = EventLoopGroupConnectionPool( 317 | source: foo, 318 | maxConnectionsPerEventLoop: 2, 319 | on: self.group 320 | ) 321 | 322 | let connection = try pool.requestConnection().wait() 323 | 324 | XCTAssertNoThrow(try pool.syncShutdownGracefully()) 325 | XCTAssertThrowsError(try pool.syncShutdownGracefully()) { 326 | XCTAssertEqual($0 as? ConnectionPoolError, ConnectionPoolError.shutdown) 327 | } 328 | XCTAssertFalse(try connection.eventLoop.submit { connection.isClosed }.wait()) 329 | pool.releaseConnection(connection) 330 | XCTAssertTrue(try connection.eventLoop.submit { connection.isClosed }.wait()) 331 | } 332 | 333 | func testEventLoopDelegation() throws { 334 | let foo = FooDatabase() 335 | let pool = EventLoopGroupConnectionPool( 336 | source: foo, 337 | maxConnectionsPerEventLoop: 1, 338 | on: self.group 339 | ) 340 | defer { pool.shutdown() } 341 | 342 | for _ in 0..<500 { 343 | let eventLoop = self.group.any() 344 | let a = pool.requestConnection( 345 | on: eventLoop 346 | ).map { conn in 347 | XCTAssertTrue(eventLoop.inEventLoop) 348 | pool.releaseConnection(conn) 349 | } 350 | let b = pool.requestConnection( 351 | on: eventLoop 352 | ).map { conn in 353 | XCTAssertTrue(eventLoop.inEventLoop) 354 | pool.releaseConnection(conn) 355 | } 356 | _ = try a.and(b).wait() 357 | } 358 | } 359 | } 360 | 361 | struct ErrorDatabase: ConnectionPoolSource { 362 | enum Error: Swift.Error { 363 | case test 364 | } 365 | 366 | func makeConnection(logger: Logger, on eventLoop: any EventLoop) -> EventLoopFuture { 367 | return eventLoop.makeFailedFuture(Error.test) 368 | } 369 | } 370 | 371 | final class FooDatabase: ConnectionPoolSource { 372 | var connectionsCreated: ManagedAtomic 373 | 374 | init() { 375 | self.connectionsCreated = .init(0) 376 | } 377 | 378 | func makeConnection(logger: Logger, on eventLoop: any EventLoop) -> EventLoopFuture { 379 | let conn = FooConnection(on: eventLoop) 380 | self.connectionsCreated.wrappingIncrement(by: 1, ordering: .relaxed) 381 | return conn.eventLoop.makeSucceededFuture(conn) 382 | } 383 | } 384 | 385 | final class FooConnection: ConnectionPoolItem, AtomicReference, @unchecked Sendable { 386 | var isClosed: Bool 387 | let eventLoop: any EventLoop 388 | 389 | init(on eventLoop: any EventLoop) { 390 | self.eventLoop = eventLoop 391 | self.isClosed = false 392 | } 393 | 394 | func close() -> EventLoopFuture { 395 | self.isClosed = true 396 | return self.eventLoop.makeSucceededFuture(()) 397 | } 398 | } 399 | 400 | /*func performance(expected seconds: Double, name: String = #function) -> Bool { 401 | #if DEBUG 402 | // guard !_isDebugAssertConfiguration() else { 403 | print("[PERFORMANCE] Skipping \(name) in debug build mode") 404 | return false 405 | // } 406 | #else 407 | print("[PERFORMANCE] \(name) expected: \(seconds) seconds") 408 | return true 409 | #endif 410 | }*/ 411 | -------------------------------------------------------------------------------- /Sources/AsyncKit/ConnectionPool/EventLoopGroupConnectionPool.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | import NIOConcurrencyHelpers 3 | import NIOCore 4 | import struct Logging.Logger 5 | 6 | /// Holds a collection of connection pools for each `EventLoop` on an `EventLoopGroup`. 7 | /// 8 | /// Connection pools are used to offset the overhead of creating new connections. Newly 9 | /// opened connections are returned back to the pool and can be re-used until they 10 | /// close. 11 | /// 12 | /// New connections are created as needed until the maximum configured connection limit 13 | /// is reached. After the maximum is reached, no new connections will be created unless 14 | /// existing connections are closed. 15 | /// 16 | /// ```swift 17 | /// let pool = EventLoopGroupConnectionPool(...) 18 | /// pool.withConnection { conn in 19 | /// // use conn 20 | /// } 21 | /// ``` 22 | public final class EventLoopGroupConnectionPool where Source: ConnectionPoolSource { 23 | /// Creates new connections when needed. See ``ConnectionPoolSource``. 24 | public let source: Source 25 | 26 | /// Limits the maximum number of connections that can be open at a given time 27 | /// for a single connection pool. 28 | public let maxConnectionsPerEventLoop: Int 29 | 30 | /// Event loop source when not specified. 31 | public let eventLoopGroup: any EventLoopGroup 32 | 33 | // MARK: Private 34 | 35 | /// For lifecycle logs. 36 | private let logger: Logger 37 | 38 | /// Synchronize access. 39 | private let lock: NIOLock 40 | 41 | /// If `true`, this connection pool has been closed. 42 | private var didShutdown: Bool 43 | 44 | /// Actual connection pool storage. 45 | private let storage: [ObjectIdentifier: EventLoopConnectionPool] 46 | 47 | /// Creates a new ``EventLoopGroupConnectionPool``. 48 | /// 49 | /// ```swift 50 | /// let pool = EventLoopGroupConnectionPool(...) 51 | /// pool.withConnection(...) { conn in 52 | /// // use conn 53 | /// } 54 | /// ``` 55 | /// 56 | /// - Parameters: 57 | /// - source: Creates new connections when needed. 58 | /// - maxConnectionsPerEventLoop: Limits the number of connections that can be open per event loop. 59 | /// Defaults to 1. 60 | /// - requestTimeout: Timeout for requesting a new connection. Defaults to 10 seconds. 61 | /// - logger: For lifecycle logs. 62 | /// - eventLoopGroup: Event loop group. 63 | public convenience init( 64 | source: Source, 65 | maxConnectionsPerEventLoop: Int = 1, 66 | requestTimeout: TimeAmount = .seconds(10), 67 | logger: Logger = .init(label: "codes.vapor.pool"), 68 | on eventLoopGroup: any EventLoopGroup 69 | ) { 70 | self.init( 71 | source: source, 72 | maxConnectionsPerEventLoop: maxConnectionsPerEventLoop, 73 | requestTimeout: requestTimeout, 74 | pruneInterval: nil, 75 | logger: logger, 76 | on: eventLoopGroup 77 | ) 78 | } 79 | 80 | /// Creates a new ``EventLoopGroupConnectionPool``. 81 | /// 82 | /// ```swift 83 | /// let pool = EventLoopGroupConnectionPool(...) 84 | /// pool.withConnection(...) { conn in 85 | /// // use conn 86 | /// } 87 | /// ``` 88 | /// 89 | /// - Parameters: 90 | /// - source: Creates new connections when needed. 91 | /// - maxConnectionsPerEventLoop: Limits the number of connections that can be open per event loop. 92 | /// Defaults to 1. 93 | /// - requestTimeout: Timeout for requesting a new connection. Defaults to 10 seconds. 94 | /// - pruneInterval: How often to check for and prune idle database connections. If `nil` (the default), 95 | /// no pruning is performed. 96 | /// - maxIdleTimeBeforePruning: How long a connection may remain idle before being pruned, if pruning is enabled. 97 | /// Defaults to 2 minutes. Ignored if `pruneInterval` is `nil`. 98 | /// - logger: For lifecycle logs. 99 | /// - eventLoopGroup: Event loop group. 100 | public init( 101 | source: Source, 102 | maxConnectionsPerEventLoop: Int = 1, 103 | requestTimeout: TimeAmount = .seconds(10), 104 | pruneInterval: TimeAmount?, 105 | maxIdleTimeBeforePruning: TimeAmount = .seconds(120), 106 | logger: Logger = .init(label: "codes.vapor.pool"), 107 | on eventLoopGroup: any EventLoopGroup 108 | ) { 109 | self.source = source 110 | self.maxConnectionsPerEventLoop = maxConnectionsPerEventLoop 111 | self.logger = logger 112 | self.lock = .init() 113 | self.eventLoopGroup = eventLoopGroup 114 | self.didShutdown = false 115 | self.storage = .init(uniqueKeysWithValues: eventLoopGroup.makeIterator().map { (.init($0), .init( 116 | source: source, 117 | maxConnections: maxConnectionsPerEventLoop, 118 | requestTimeout: requestTimeout, 119 | pruneInterval: pruneInterval, 120 | maxIdleTimeBeforePruning: maxIdleTimeBeforePruning, 121 | logger: logger, 122 | on: $0 123 | )) }) 124 | } 125 | 126 | /// Fetches a pooled connection for the lifetime of the closure. 127 | /// 128 | /// The connection is provided to the supplied callback and will be automatically released when the 129 | /// future returned by the callback is completed. 130 | /// 131 | /// ```swift 132 | /// pool.withConnection(...) { conn in 133 | /// // use the connection 134 | /// } 135 | /// ``` 136 | /// 137 | /// See ``EventLoopGroupConnectionPool/requestConnection(logger:on:)`` to request a pooled connection without 138 | /// using a callback. 139 | /// 140 | /// - Parameters: 141 | /// - logger: For trace and debug logs. 142 | /// - eventLoop: Preferred event loop for the new connection. 143 | /// - closure: Callback that accepts the pooled connection. 144 | /// - Returns: A future containing the result of the closure. 145 | public func withConnection( 146 | logger: Logger? = nil, 147 | on eventLoop: (any EventLoop)? = nil, 148 | _ closure: @escaping (Source.Connection) -> EventLoopFuture 149 | ) -> EventLoopFuture { 150 | guard !self.lock.withLock({ self.didShutdown }) else { 151 | return (eventLoop ?? self.eventLoopGroup).future(error: ConnectionPoolError.shutdown) 152 | } 153 | return self.pool(for: eventLoop ?? self.eventLoopGroup.any()) 154 | .withConnection(logger: logger ?? self.logger, closure) 155 | } 156 | 157 | /// Requests a pooled connection. 158 | /// 159 | /// The connection returned by this method should be released when you are finished using it. 160 | /// 161 | /// ```swift 162 | /// let conn = try pool.requestConnection(...).wait() 163 | /// defer { pool.releaseConnection(conn) } 164 | /// // use the connection 165 | /// ``` 166 | /// 167 | /// See ``EventLoopGroupConnectionPool/withConnection(logger:on:_:)`` for a callback-based method that automatically 168 | /// releases the connection. 169 | /// 170 | /// - Parameters: 171 | /// - logger: For trace and debug logs. 172 | /// - eventLoop: Preferred event loop for the new connection. 173 | /// - Returns: A future containing the requested connection. 174 | public func requestConnection( 175 | logger: Logger? = nil, 176 | on eventLoop: (any EventLoop)? = nil 177 | ) -> EventLoopFuture { 178 | guard !self.lock.withLock({ self.didShutdown }) else { 179 | return (eventLoop ?? self.eventLoopGroup).future(error: ConnectionPoolError.shutdown) 180 | } 181 | return self.pool(for: eventLoop ?? self.eventLoopGroup.any()) 182 | .requestConnection(logger: logger ?? self.logger) 183 | } 184 | 185 | /// Releases a connection back to the pool. Use with ``EventLoopGroupConnectionPool/requestConnection(logger:on:)``. 186 | /// 187 | /// ```swift 188 | /// let conn = try pool.requestConnection(...).wait() 189 | /// defer { pool.releaseConnection(conn) } 190 | /// // use the connection 191 | /// ``` 192 | /// 193 | /// - Parameters: 194 | /// - connection: Connection to release back to the pool. 195 | /// - logger: For trace and debug logs. 196 | public func releaseConnection( 197 | _ connection: Source.Connection, 198 | logger: Logger? = nil 199 | ) { 200 | self.pool(for: connection.eventLoop) 201 | .releaseConnection(connection, logger: logger ?? self.logger) 202 | } 203 | 204 | /// Returns the ``EventLoopConnectionPool`` for a specific event loop. 205 | public func pool(for eventLoop: any EventLoop) -> EventLoopConnectionPool { 206 | self.storage[.init(eventLoop)]! 207 | } 208 | 209 | /// Closes the connection pool. 210 | /// 211 | /// All available connections will be closed immediately. 212 | /// Any connections currently in use will be closed when they are returned to the pool. 213 | /// 214 | /// Once closed, the connection pool cannot be used to create new connections. 215 | /// 216 | /// Connection pools must be closed before they deinitialize. 217 | /// 218 | /// > Warning: This method is soft-deprecated. Use ``EventLoopGroupConnectionPool/syncShutdownGracefully()`` or 219 | /// > ``EventLoopGroupConnectionPool/shutdownGracefully(_:)`` instead. 220 | @available(*, noasync, message: "This calls wait() and should not be used in an async context", renamed: "shutdownAsync()") 221 | public func shutdown() { 222 | // synchronize access to closing 223 | guard self.lock.withLock({ 224 | // check to make sure we aren't double closing 225 | guard !self.didShutdown else { 226 | return false 227 | } 228 | self.didShutdown = true 229 | self.logger.debug("Connection pool shutting down, closing each event loop's storage") 230 | return true 231 | }) else { 232 | return 233 | } 234 | 235 | // shutdown all pools 236 | for pool in self.storage.values { 237 | do { 238 | try pool.close().wait() 239 | } catch { 240 | self.logger.error("Failed shutting down event loop pool: \(error)") 241 | } 242 | } 243 | } 244 | 245 | /// Closes the connection pool. 246 | /// 247 | /// All available connections will be closed immediately. Any connections still in use will be 248 | /// closed as soon as they are returned to the pool. Once closed, the pool can not be used to 249 | /// create new connections. 250 | /// 251 | /// Connection pools must be closed before they deinitialize. 252 | /// 253 | /// This method shuts down asynchronously, waiting for all connection closures to complete before 254 | /// returning. 255 | /// 256 | /// > Warning: The pool is always fully shut down once this method returns, even if an error is 257 | /// > thrown. All errors are purely advisory. 258 | public func shutdownAsync() async throws { 259 | // synchronize access to closing 260 | guard self.lock.withLock({ 261 | // check to make sure we aren't double closing 262 | guard !self.didShutdown else { 263 | return false 264 | } 265 | self.didShutdown = true 266 | self.logger.debug("Connection pool shutting down, closing each event loop's storage") 267 | return true 268 | }) else { 269 | self.logger.debug("Cannot shutdown the connection pool more than once") 270 | throw ConnectionPoolError.shutdown 271 | } 272 | 273 | // shutdown all pools 274 | for pool in self.storage.values { 275 | do { 276 | try await pool.close().get() 277 | } catch { 278 | self.logger.error("Failed shutting down event loop pool: \(error)") 279 | } 280 | } 281 | } 282 | 283 | /// Closes the connection pool. 284 | /// 285 | /// All available connections will be closed immediately. Any connections still in use will be 286 | /// closed as soon as they are returned to the pool. Once closed, the pool can not be used to 287 | /// create new connections. 288 | /// 289 | /// Connection pools must be closed before they deinitialize. 290 | /// 291 | /// This method shuts down synchronously, waiting for all connection closures to complete before 292 | /// returning. 293 | /// 294 | /// > Warning: The pool is always fully shut down once this method returns, even if an error is 295 | /// > thrown. All errors are purely advisory. 296 | @available(*, noasync, message: "This calls wait() and should not be used in an async context", renamed: "shutdownAsync()") 297 | public func syncShutdownGracefully() throws { 298 | var possibleError: (any Error)? = nil 299 | let waiter = DispatchSemaphore(value: 0) 300 | let errorLock = NIOLock() 301 | 302 | self.shutdownGracefully { 303 | if let error = $0 { 304 | errorLock.withLock { possibleError = error } 305 | } 306 | waiter.signal() 307 | } 308 | waiter.wait() 309 | try errorLock.withLock { 310 | if let error = possibleError { 311 | throw error 312 | } 313 | } 314 | } 315 | 316 | /// Closes the connection pool. 317 | /// 318 | /// All available connections will be closed immediately. Any connections still in use will be 319 | /// closed as soon as they are returned to the pool. Once closed, the pool can not be used to 320 | /// create new connections. 321 | /// 322 | /// Connection pools must be closed before they deinitialize. 323 | /// 324 | /// This method shuts the pool down asynchronously. It may be invoked on any event loop. The 325 | /// provided callback will be notified when shutdown is complete. It is invalid to allow a pool 326 | /// to deinitialize before it has fully shut down. 327 | /// 328 | /// This method promises explicitly as API contract not to invoke the callback before returning 329 | /// to its caller. It further promises the callback will not be invoked on any event loop 330 | /// belonging to the pool. 331 | /// 332 | /// > Warning: Any invocation of the callback represents a signal that the pool has fully shut 333 | /// > down. This is true even if the error parameter is non-`nil`; errors are purely advisory. 334 | public func shutdownGracefully(_ callback: @escaping ((any Error)?) -> Void) { 335 | // Protect access to shared state. 336 | guard self.lock.withLock({ 337 | // Do not initiate shutdown multiple times. 338 | guard !self.didShutdown else { 339 | DispatchQueue.global().async { 340 | self.logger.warning("Connection pool can not be shut down more than once.") 341 | callback(ConnectionPoolError.shutdown) 342 | } 343 | return false 344 | } 345 | 346 | // Set the flag as soon as we know a shutdown is needed. 347 | self.didShutdown = true 348 | self.logger.trace("Connection group pool shutdown start - telling the loop pools what's up.") 349 | // Don't need to hold the lock anymore; the shutdown can proceed without blocking anything else, though 350 | // it's also true there's nothing else to block that we care about after shutdown begin. 351 | return true 352 | }) else { return } 353 | 354 | // Tell each pool to shut down and take note of any errors if they show up. Use the dispatch 355 | // queue to manage synchronization to avoid being trapped on any of our own event loops. When 356 | // all pools are closed, invoke the callback and provide it the first encountered error, if 357 | // any. By design, this loosely matches the general flow used by `MultiThreadedEventLoopGroup`'s 358 | // `shutdownGracefully(queue:_:)` implementation. 359 | let shutdownQueue = DispatchQueue(label: "codes.vapor.async-kit.poolShutdownGracefullyQueue") 360 | let shutdownGroup = DispatchGroup() 361 | var outcome: Result = .success(()) 362 | 363 | for pool in self.storage.values { 364 | shutdownGroup.enter() 365 | pool.close().whenComplete { result in 366 | shutdownQueue.async { 367 | outcome = outcome.flatMap { result } 368 | shutdownGroup.leave() 369 | } 370 | } 371 | } 372 | 373 | shutdownGroup.notify(queue: shutdownQueue) { 374 | switch outcome { 375 | case .success: 376 | self.logger.debug("Connection group pool finished shutdown.") 377 | callback(nil) 378 | case .failure(let error): 379 | self.logger.error("Connection group pool got shutdown error (and then shut down anyway): \(error)") 380 | callback(error) 381 | } 382 | } 383 | } 384 | 385 | deinit { 386 | assert(self.lock.withLock { self.didShutdown }, "ConnectionPool.shutdown() was not called before deinit.") 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /Sources/AsyncKit/ConnectionPool/EventLoopConnectionPool.swift: -------------------------------------------------------------------------------- 1 | import Atomics 2 | import Collections 3 | import NIOCore 4 | import struct Foundation.UUID 5 | import struct Logging.Logger 6 | 7 | /// Holds a collection of active connections that can be requested and later released 8 | /// back into the pool. 9 | /// 10 | /// Connection pools are used to offset the overhead of creating new connections. Newly 11 | /// opened connections are returned back to the pool and can be re-used until they 12 | /// close. 13 | /// 14 | /// New connections are created as needed until the maximum configured connection limit 15 | /// is reached. After the maximum is reached, no new connections will be created unless 16 | /// existing connections are closed. 17 | /// 18 | /// ```swift 19 | /// let pool = EventLoopConnectionPool(...) 20 | /// pool.withConnection(...) { conn in 21 | /// // use conn 22 | /// } 23 | /// ``` 24 | public final class EventLoopConnectionPool where Source: ConnectionPoolSource { 25 | private typealias WaitlistItem = (logger: Logger, promise: EventLoopPromise, timeoutTask: Scheduled) 26 | 27 | /// Connection source. 28 | public let source: Source 29 | 30 | /// Max connections for this pool. 31 | private let maxConnections: Int 32 | 33 | /// Timeout for requesting a new connection. 34 | private let requestTimeout: TimeAmount 35 | 36 | /// This pool's event loop. 37 | public let eventLoop: any EventLoop 38 | 39 | /// ID generator 40 | private let idGenerator = ManagedAtomic(0) 41 | 42 | /// All currently available connections, paired with their last usage times. 43 | /// 44 | /// > Note: Any connection in this list may have become invalid since its last use. 45 | private var available: Deque<(connection: Source.Connection, lastUse: NIODeadline)> 46 | 47 | /// Current active connection count. 48 | private var activeConnections: Int 49 | 50 | /// Connection requests waiting to be fulfilled due to pool exhaustion. 51 | private var waiters: OrderedDictionary 52 | 53 | /// If `true`, this storage has been shutdown. 54 | private var didShutdown: Bool 55 | 56 | /// For lifecycle logs. 57 | public let logger: Logger 58 | 59 | /// Pruning interval, if enabled. 60 | private let pruningInterval: TimeAmount? 61 | 62 | /// Max connection idle time before pruning, if enabled. 63 | private let maxIdleTimeBeforePruning: TimeAmount 64 | 65 | /// Connection pruning timer, if pruning is enabled. 66 | private var pruningTask: RepeatedTask? 67 | 68 | /// Creates a new ``EventLoopConnectionPool``. 69 | /// 70 | /// ```swift 71 | /// let pool = EventLoopConnectionPool(...) 72 | /// pool.withConnection(...) { conn in 73 | /// // use conn 74 | /// } 75 | /// ``` 76 | /// 77 | /// - Parameters: 78 | /// - source: Creates new connections when needed. 79 | /// - maxConnections: Limits the number of connections that can be open. Defaults to 1. 80 | /// - requestTimeout: Timeout for requesting a new connection. Defaults to 10 seconds. 81 | /// - logger: For lifecycle logs. 82 | /// - on: Event loop. 83 | public convenience init( 84 | source: Source, 85 | maxConnections: Int, 86 | requestTimeout: TimeAmount = .seconds(10), 87 | logger: Logger = .init(label: "codes.vapor.pool"), 88 | on eventLoop: any EventLoop 89 | ) { 90 | self.init( 91 | source: source, 92 | maxConnections: maxConnections, 93 | requestTimeout: requestTimeout, 94 | pruneInterval: nil, 95 | logger: logger, 96 | on: eventLoop 97 | ) 98 | } 99 | 100 | /// Creates a new ``EventLoopConnectionPool``. 101 | /// 102 | /// ```swift 103 | /// let pool = EventLoopConnectionPool(...) 104 | /// pool.withConnection(...) { conn in 105 | /// // use conn 106 | /// } 107 | /// ``` 108 | /// 109 | /// - Parameters: 110 | /// - source: Creates new connections when needed. 111 | /// - maxConnections: Limits the number of connections that can be open. Defaults to 1. 112 | /// - requestTimeout: Timeout for requesting a new connection. Defaults to 10 seconds. 113 | /// - pruneInterval: How often to check for and prune idle database connections. If `nil` (the default), 114 | /// no pruning is performed. 115 | /// - maxIdleTimeBeforePruning: How long a connection may remain idle before being pruned, if pruning is enabled. 116 | /// Defaults to 2 minutes. Ignored if `pruneInterval` is `nil`. 117 | /// - logger: For lifecycle logs. 118 | /// - on: Event loop. 119 | public init( 120 | source: Source, 121 | maxConnections: Int, 122 | requestTimeout: TimeAmount = .seconds(10), 123 | pruneInterval: TimeAmount?, 124 | maxIdleTimeBeforePruning: TimeAmount = .seconds(120), 125 | logger: Logger = .init(label: "codes.vapor.pool"), 126 | on eventLoop: any EventLoop 127 | ) { 128 | self.source = source 129 | self.maxConnections = maxConnections 130 | self.requestTimeout = requestTimeout 131 | self.pruningInterval = pruneInterval 132 | self.maxIdleTimeBeforePruning = maxIdleTimeBeforePruning 133 | self.logger = logger 134 | self.eventLoop = eventLoop 135 | self.available = .init(minimumCapacity: maxConnections) 136 | self.activeConnections = 0 137 | self.waiters = .init(minimumCapacity: maxConnections << 2) 138 | self.didShutdown = false 139 | } 140 | 141 | /// Fetches a pooled connection for the lifetime of the closure. 142 | /// 143 | /// The connection is provided to the supplied callback and will be automatically released when the 144 | /// future returned by the callback is completed. 145 | /// 146 | /// ```swift 147 | /// pool.withConnection { conn in 148 | /// // use the connection 149 | /// } 150 | /// ``` 151 | /// 152 | /// See ``EventLoopConnectionPool/requestConnection()`` to request a pooled connection without using a callback. 153 | /// 154 | /// - Parameters: 155 | /// - closure: Callback that accepts the pooled connection. 156 | /// - Returns: A future containing the result of the closure. 157 | public func withConnection( 158 | _ closure: @escaping (Source.Connection) -> EventLoopFuture 159 | ) -> EventLoopFuture { 160 | self.withConnection(logger: self.logger, closure) 161 | } 162 | 163 | /// Fetches a pooled connection for the lifetime of the closure. 164 | /// 165 | /// The connection is provided to the supplied callback and will be automatically released when the 166 | /// future returned by the callback is completed. 167 | /// 168 | /// ```swift 169 | /// pool.withConnection(...) { conn in 170 | /// // use the connection 171 | /// } 172 | /// ``` 173 | /// 174 | /// See ``EventLoopConnectionPool/requestConnection(logger:)`` to request a pooled connection without using a callback. 175 | /// 176 | /// - Parameters: 177 | /// - logger: For trace and debug logs. 178 | /// - closure: Callback that accepts the pooled connection. 179 | /// - Returns: A future containing the result of the closure. 180 | public func withConnection( 181 | logger: Logger, 182 | _ closure: @escaping (Source.Connection) -> EventLoopFuture 183 | ) -> EventLoopFuture { 184 | self.requestConnection(logger: logger).flatMap { conn in 185 | closure(conn).always { _ in 186 | self.releaseConnection(conn, logger: logger) 187 | } 188 | } 189 | } 190 | 191 | /// Requests a pooled connection. 192 | /// 193 | /// The connection returned by this method MUST be released when you are finished using it. 194 | /// 195 | /// ```swift 196 | /// let conn = try pool.requestConnection(...).wait() 197 | /// defer { pool.releaseConnection(conn) } 198 | /// // use the connection 199 | /// ``` 200 | /// 201 | /// See ``EventLoopConnectionPool/withConnection(_:)`` for a callback-based method that automatically releases the connection. 202 | /// 203 | /// - Returns: A future containing the requested connection. 204 | public func requestConnection() -> EventLoopFuture { 205 | self.requestConnection(logger: self.logger) 206 | } 207 | 208 | /// Requests a pooled connection. 209 | /// 210 | /// The connection returned by this method MUST be released when you are finished using it. 211 | /// 212 | /// ```swift 213 | /// let conn = try pool.requestConnection(...).wait() 214 | /// defer { pool.releaseConnection(conn) } 215 | /// // use the connection 216 | /// ``` 217 | /// 218 | /// See ``EventLoopConnectionPool/withConnection(logger:_:)`` for a callback-based method that automatically releases the connection. 219 | /// 220 | /// - Parameters: 221 | /// - logger: For trace and debug logs. 222 | /// - eventLoop: Preferred event loop for the new connection. 223 | /// - Returns: A future containing the requested connection. 224 | public func requestConnection(logger: Logger) -> EventLoopFuture { 225 | /// N.B.: This particular pattern (the use of a promise to forward the result when off the event loop) 226 | /// is straight out of NIO's `EventLoopFuture.fold()` implementation. 227 | if self.eventLoop.inEventLoop { 228 | return self._requestConnection0(logger: logger) 229 | } else { 230 | let promise = self.eventLoop.makePromise(of: Source.Connection.self) 231 | self.eventLoop.execute { self._requestConnection0(logger: logger).cascade(to: promise) } 232 | return promise.futureResult 233 | } 234 | } 235 | 236 | /// Actual implementation of ``EventLoopConnectionPool/requestConnection(logger:)``. 237 | private func _requestConnection0(logger: Logger) -> EventLoopFuture { 238 | self.eventLoop.assertInEventLoop() 239 | 240 | guard !self.didShutdown else { 241 | return self.eventLoop.makeFailedFuture(ConnectionPoolError.shutdown) 242 | } 243 | 244 | // Find an available connection that isn't closed 245 | while let conn = self.available.popLast() { 246 | if !conn.connection.isClosed { 247 | logger.trace("Using available connection") 248 | return self.eventLoop.makeSucceededFuture(conn.connection) 249 | } else { 250 | logger.debug("Pruning defunct connection") 251 | self.activeConnections -= 1 252 | } 253 | } 254 | 255 | // Put the current request on the waiter list in case opening a new connection is slow 256 | let waiterId = self.idGenerator.wrappingIncrementThenLoad(ordering: .relaxed) 257 | let promise = self.eventLoop.makePromise(of: Source.Connection.self) 258 | let timeoutTask = self.eventLoop.scheduleTask(in: self.requestTimeout) { [weak self] in 259 | // Try to avoid a spurious log message and failure if the waiter has already been removed from the list. 260 | guard self?.waiters.removeValue(forKey: waiterId) != nil else { 261 | logger.trace("Waiter already removed when timeout task fired", metadata: ["waiter": .stringConvertible(waiterId)]) 262 | return 263 | } 264 | logger.error(""" 265 | Connection request timed out. This might indicate a connection deadlock in \ 266 | your application. If you have long-running requests, consider increasing your connection timeout. 267 | """, 268 | metadata: ["waiter": .stringConvertible(waiterId)] 269 | ) 270 | promise.fail(ConnectionPoolTimeoutError.connectionRequestTimeout) 271 | } 272 | logger.trace("Adding connection request to waitlist", metadata: ["waiter": .stringConvertible(waiterId)]) 273 | self.waiters[waiterId] = (logger: logger, promise: promise, timeoutTask: timeoutTask) 274 | 275 | promise.futureResult.whenComplete { [weak self] _ in 276 | logger.trace("Connection request completed", metadata: ["waiter": .stringConvertible(waiterId)]) 277 | timeoutTask.cancel() 278 | self?.waiters.removeValue(forKey: waiterId) 279 | } 280 | 281 | // If the pool isn't full, attempt to open a new connection 282 | if self.activeConnections < self.maxConnections { 283 | logger.trace("Attemping new connection for pool") 284 | self.activeConnections += 1 285 | self.source.makeConnection(logger: logger, on: self.eventLoop).map { 286 | // On success, "release" the new connection to the pool and let the waitlist logic take over 287 | logger.trace("New connection successful, servicing waitlist") 288 | self._releaseConnection0($0, logger: logger) 289 | }.flatMapErrorWithEventLoop { [weak self] error, eventLoop in 290 | self?.activeConnections -= 1 291 | logger.error("Opening new connection for pool failed", metadata: ["error": .string(String(reflecting: error))]) 292 | return eventLoop.makeFailedFuture(error) 293 | }.cascadeFailure(to: promise) 294 | } 295 | 296 | return promise.futureResult 297 | } 298 | 299 | /// Releases a connection back to the pool. Use with ``EventLoopConnectionPool/requestConnection()``. 300 | /// 301 | /// ```swift 302 | /// let conn = try pool.requestConnection().wait() 303 | /// defer { pool.releaseConnection(conn) } 304 | /// // use the connection 305 | /// ``` 306 | /// 307 | /// - Parameters: 308 | /// - connection: Connection to release back to the pool. 309 | public func releaseConnection(_ connection: Source.Connection) { 310 | self.releaseConnection(connection, logger: self.logger) 311 | } 312 | 313 | /// Releases a connection back to the pool. Use with ``EventLoopConnectionPool/requestConnection(logger:)``. 314 | /// 315 | /// ```swift 316 | /// let conn = try pool.requestConnection().wait() 317 | /// defer { pool.releaseConnection(conn) } 318 | /// // use the connection 319 | /// ``` 320 | /// 321 | /// - Parameters: 322 | /// - connection: Connection to release back to the pool. 323 | /// - logger: For trace and debug logs. 324 | public func releaseConnection(_ connection: Source.Connection, logger: Logger) { 325 | if self.eventLoop.inEventLoop { 326 | self._releaseConnection0(connection, logger: logger) 327 | } else { 328 | self.eventLoop.execute { self._releaseConnection0(connection, logger: logger) } 329 | } 330 | } 331 | 332 | private func _releaseConnection0(_ connection: Source.Connection, logger: Logger) { 333 | self.eventLoop.assertInEventLoop() 334 | 335 | // If the pool has shut down, just close the connection and return 336 | guard !self.didShutdown else { 337 | if !connection.isClosed { 338 | _ = connection.close() 339 | } 340 | return 341 | } 342 | 343 | logger.trace("Releasing pool connection.", metadata: [ 344 | "eventloop": .string(self.eventLoop.description 345 | .components(separatedBy: "thread = NIOThread(name = ").dropFirst().first? 346 | .components(separatedBy: ")").first ?? "" 347 | ), 348 | "available": .stringConvertible(self.available.count + (connection.isClosed ? 0 : 1)) 349 | ]) 350 | 351 | // Push the connection onto the end of the available list so it's the first one to get used 352 | // on the next request. Do this even if the connection is closed in order to ensure we service 353 | // the waitlist and start a new connection if needed. 354 | self.available.append((connection: connection, lastUse: .now())) 355 | 356 | if self.available.count == 1, let pruningInterval = self.pruningInterval, self.pruningTask == nil { // just added our very first connection 357 | self.pruningTask = self.eventLoop.scheduleRepeatedTask( 358 | initialDelay: pruningInterval, 359 | delay: pruningInterval 360 | ) { [unowned self] _ in 361 | var prunedConnections: Set = [] 362 | 363 | for conninfo in self.available where !conninfo.connection.isClosed { 364 | if conninfo.lastUse + self.maxIdleTimeBeforePruning < .now() { 365 | self.logger.debug("Pruning idle connection.") 366 | _ = conninfo.connection.close() 367 | prunedConnections.insert(.init(conninfo.connection)) 368 | } 369 | } 370 | if !prunedConnections.isEmpty { 371 | self.available.removeAll(where: { prunedConnections.contains(ObjectIdentifier($0.connection)) }) 372 | self.activeConnections -= prunedConnections.count 373 | } 374 | } 375 | } 376 | 377 | // For as long as there are connections available, try to dequeue waiters. Even if the available 378 | // connection(s) are closed, the request logic will try to open new ones. 379 | while !self.available.isEmpty, !self.waiters.isEmpty { 380 | let waiter = self.waiters.removeFirst() 381 | 382 | logger.debug("Servicing connection waitlist item", metadata: ["waiter": .stringConvertible(waiter.key)]) 383 | waiter.value.timeoutTask.cancel() 384 | self._requestConnection0(logger: waiter.value.logger).cascade(to: waiter.value.promise) 385 | } 386 | } 387 | 388 | /// Closes the connection pool. 389 | /// 390 | /// All available connections will be closed immediately. 391 | /// Any connections currently in use will be closed when they are returned to the pool. 392 | /// 393 | /// Once closed, the connection pool cannot be used to create new connections. 394 | /// 395 | /// Connection pools must be closed before they deinitialize. 396 | public func close() -> EventLoopFuture { 397 | if self.eventLoop.inEventLoop { 398 | return self._close0() 399 | } else { 400 | let promise = self.eventLoop.makePromise(of: Void.self) 401 | self.eventLoop.execute { self._close0().cascade(to: promise) } 402 | return promise.futureResult 403 | } 404 | } 405 | 406 | private func _close0() -> EventLoopFuture { 407 | self.eventLoop.assertInEventLoop() 408 | 409 | guard !self.didShutdown else { 410 | return self.eventLoop.makeSucceededVoidFuture() 411 | } 412 | self.didShutdown = true 413 | self.logger.trace("Connection pool shutting down - closing all available connections on this event loop") 414 | 415 | let pruningCancellationPromise = self.eventLoop.makePromise(of: Void.self) 416 | if let pruningTask = self.pruningTask { 417 | pruningTask.cancel(promise: pruningCancellationPromise) 418 | } else { 419 | pruningCancellationPromise.succeed() 420 | } 421 | 422 | for (_, waiter) in self.waiters { 423 | waiter.timeoutTask.cancel() 424 | waiter.promise.fail(ConnectionPoolError.shutdown) 425 | } 426 | self.waiters.removeAll() 427 | 428 | return pruningCancellationPromise.futureResult.flatMap { 429 | self.available.map { 430 | $0.connection.close() 431 | }.flatten(on: self.eventLoop) 432 | }.map { 433 | self.activeConnections = 0 434 | self.available.removeAll() 435 | } 436 | } 437 | 438 | internal/*testable*/ func poolState() -> EventLoopFuture<(known: Int, active: Int, open: Int)> { 439 | if self.eventLoop.inEventLoop { 440 | self.eventLoop.makeSucceededFuture((self.available.count, self.activeConnections, self.available.filter { !$0.connection.isClosed }.count)) 441 | } else { 442 | self.eventLoop.submit { (self.available.count, self.activeConnections, self.available.filter { !$0.connection.isClosed }.count) } 443 | } 444 | } 445 | 446 | deinit { 447 | if !self.didShutdown { 448 | assertionFailure("ConnectionPoolStorage.shutdown() was not called before deinit.") 449 | } 450 | } 451 | } 452 | --------------------------------------------------------------------------------