Skip to content

Commit 3b1e7e9

Browse files
authored
Add EventLoop.performWithTask(_:) to complement EventLoopGroup.performWithTask(_:) (#87)
* Lots of cleanup * Fully migrate async-kit workflows to use the same model as postgres-nio's are using. * Tweak the deadlock test slightly since it seems to exceed 0.2 due to various issues fairly often * Add some downright silly test cases to make the code coverage stat less abysmal with all these overloads.
1 parent 748c026 commit 3b1e7e9

23 files changed

+527
-365
lines changed

.github/workflows/main-codecov.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: main codecov
2+
on:
3+
push:
4+
branches:
5+
- main
6+
workflow_dispatch:
7+
jobs:
8+
update-main-codecov:
9+
runs-on: ubuntu-latest
10+
container: swift:5.5-focal
11+
steps:
12+
- name: Check out main
13+
uses: actions/checkout@v2
14+
- name: Run tests with code coverage and Thread Sanitizer
15+
run: swift test --enable-code-coverage --sanitize=thread
16+
- name: Submit coverage report to Codecov.io
17+
uses: vapor/[email protected]
18+
with:
19+
cc_flags: 'unittests'
20+
cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH'
21+
cc_fail_ci_if_error: true
22+
cc_verbose: true

.github/workflows/test.yml

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,64 @@ name: test
22
on: { pull_request: {} }
33

44
jobs:
5-
getcidata:
5+
linux-all:
6+
strategy:
7+
fail-fast: false
8+
matrix:
9+
swiftver:
10+
- swift:5.2
11+
- swift:5.3
12+
- swift:5.4
13+
- swift:5.5
14+
- swiftlang/swift:nightly-main
15+
swiftos:
16+
- focal
17+
container: ${{ format('{0}-{1}', matrix.swiftver, matrix.swiftos) }}
618
runs-on: ubuntu-latest
7-
outputs:
8-
environments: ${{ steps.output.outputs.environments }}
19+
env:
20+
LOG_LEVEL: debug
921
steps:
10-
- id: output
11-
run: |
12-
envblob="$(curl -fsSL https://raw.githubusercontent.com/vapor/ci/main/pr-environments.json | jq -cMj '.')"
13-
echo "::set-output name=environments::${envblob}"
22+
- name: Check out package
23+
uses: actions/checkout@v2
24+
- name: Run tests with code coverage and Thread Sanitizer
25+
run: swift test --enable-test-discovery --sanitize=thread --enable-code-coverage
26+
- name: Submit coverage report to Codecov.io
27+
uses: vapor/[email protected]
28+
with:
29+
cc_flags: 'unittests'
30+
cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH'
31+
cc_fail_ci_if_error: true
32+
cc_verbose: true
1433

15-
test-asynckit:
16-
needs: getcidata
34+
macos-all:
1735
strategy:
1836
fail-fast: false
1937
matrix:
20-
env: ${{ fromJSON(needs.getcidata.outputs.environments) }}
21-
runs-on: ${{ matrix.env.os }}
22-
container: ${{ matrix.env.image }}
38+
xcode:
39+
- latest-stable
40+
- latest
41+
include:
42+
- xcode: latest-stable
43+
filter: '--skip=^AsyncKitTests.EventLoopConcurrencyTests'
44+
runs-on: macos-11
2345
env:
24-
SWIFT_RUNNER: ${{ format('{0}-{1}-{2}', matrix.env.os, matrix.env.toolchain, matrix.env.image) }}
25-
steps:
26-
- name: Select toolchain
27-
uses: maxim-lobanov/setup-xcode@v1.2.1
46+
LOG_LEVEL: debug
47+
steps:
48+
- name: Select latest available Xcode
49+
uses: maxim-lobanov/setup-xcode@v1
2850
with:
29-
xcode-version: ${{ matrix.env.toolchain }}
30-
if: ${{ matrix.env.toolchain != '' }}
31-
- name: Ensure curl's availability
32-
run: apt-get update && apt-get install -y curl
33-
if: ${{ matrix.env.image != '' }}
34-
- name: Check out code
51+
xcode-version: ${{ matrix.xcode }}
52+
- name: Checkout code
3553
uses: actions/checkout@v2
36-
- name: Run tests with Thread Sanitizer
37-
timeout-minutes: 20
38-
run: swift test --enable-test-discovery --sanitize=thread --enable-code-coverage
39-
- name: Convert data to LCOV format and save location
54+
- name: Run tests with code coverage and Thread Sanitizer
4055
run: |
41-
export build=$(swift build --show-bin-path) && \
42-
export subpath="$([[ $(uname -s) == "Darwin" ]] && echo "/Contents/MacOS/async-kitPackageTests" || true)" && \
43-
export exc_prefix="$([[ $(uname -s) == "Darwin" ]] && echo "xcrun" || true)" && \
44-
${exc_prefix} llvm-cov export \
45-
-format lcov \
46-
-instr-profile=$build/codecov/default.profdata \
47-
--ignore-filename-regex='\.build/' \
48-
$build/async-kitPackageTests.xctest$subpath \
49-
>$(swift test --show-codecov-path).lcov
50-
echo "CODECOV_FILE=$(swift test --show-codecov-path).lcov" >> $GITHUB_ENV
51-
shell: bash
52-
- name: Send to codecov.io
53-
uses: codecov/[email protected]
56+
swift test --enable-test-discovery ${{ matrix.filter }} --sanitize=thread --enable-code-coverage \
57+
-Xlinker -rpath \
58+
-Xlinker $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.5/macosx
59+
- name: Submit coverage report to Codecov.io
60+
uses: vapor/[email protected]
5461
with:
55-
file: ${{ env.CODECOV_FILE }}
56-
flags: unittests
57-
env_vars: SWIFT_RUNNER
58-
fail_ci_if_error: true
62+
cc_flags: 'unittests'
63+
cc_env_vars: 'MD_APPLE_SDK_ROOT,RUNNER_OS,RUNNER_ARCH'
64+
cc_fail_ci_if_error: true
65+
cc_verbose: true
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#if compiler(>=5.5) && canImport(_Concurrency)
2+
import NIOCore
3+
4+
extension EventLoop {
5+
/// Run the `async` function `body` on this event loop and return its result as
6+
/// an `EventLoopFuture`.
7+
///
8+
/// This function can be used to bridge the `async` world into an `EventLoop`.
9+
///
10+
/// See also ``EventLoopPromise.completeWithTask(_:)``
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(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
18+
@inlinable
19+
public func performWithTask<Value>(
20+
_ body: @escaping @Sendable () async throws -> Value
21+
) -> EventLoopFuture<Value> {
22+
let promise = self.makePromise(of: Value.self)
23+
24+
promise.completeWithTask(body)
25+
return promise.futureResult
26+
}
27+
}
28+
#endif
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#if compiler(>=5.5) && canImport(_Concurrency)
2+
import NIOCore
3+
4+
extension EventLoopGroup {
5+
/// Run the `async` function `body` on an event loop in this group and return its
6+
/// result as an `EventLoopFuture`.
7+
///
8+
/// This function can be used to bridge the `async` world into an `EventLoopGroup`.
9+
///
10+
/// See also ``EventLoop.performWithTask(_:)``, ``EventLoopPromise.completeWithTask(_:)``
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(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
18+
@inlinable
19+
public func performWithTask<Value>(
20+
_ body: @escaping @Sendable () async throws -> Value
21+
) -> EventLoopFuture<Value> {
22+
return self.next().performWithTask(body)
23+
}
24+
}
25+
#endif

Sources/AsyncKit/EventLoopFuture/Future+Throwing.swift renamed to Sources/AsyncKit/EventLoop/EventLoopGroup+Throwing.swift

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,4 @@ extension EventLoopGroup {
2626
public func tryFuture<T>(_ work: @escaping () throws -> T) -> EventLoopFuture<T> {
2727
return self.next().submit(work)
2828
}
29-
30-
#if compiler(>=5.5) && canImport(_Concurrency)
31-
/// Performs an async work and returns the result in form of an `EventLoopFuture`.
32-
///
33-
/// - Parameter work: The async, potentially throwing closure to execute as a
34-
/// future. If the closure throws, a failed future is returned.
35-
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
36-
public func performWithTask<T>(
37-
_ work: @escaping @Sendable () async throws -> T
38-
) -> EventLoopFuture<T> {
39-
let promise = self.next().makePromise(of: T.self)
40-
promise.completeWithTask(work)
41-
return promise.futureResult
42-
}
43-
#endif
4429
}
45-
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
extension EventLoopFuture {
2+
// MARK: - Guard
3+
4+
/// Guards that the future's value satisfies the callback's condition or
5+
/// fails with the given error.
6+
///
7+
///
8+
/// Example usage:
9+
///
10+
/// future.guard({ $0.userID == user.id }, else: AccessError.unauthorized)
11+
///
12+
/// - parameters:
13+
/// - callback: Callback that asynchronously executes a condition.
14+
/// - error: The error to fail with if condition isn't met.
15+
/// - returns: A future containing the original future's result.
16+
public func `guard`(_ callback: @escaping ((Value) -> Bool), else error: @escaping @autoclosure () -> Error) -> EventLoopFuture<Value> {
17+
let promise = self.eventLoop.makePromise(of: Value.self)
18+
self.whenComplete { result in
19+
switch result {
20+
case .success(let value):
21+
if callback(value) {
22+
promise.succeed(value)
23+
} else {
24+
promise.fail(error())
25+
}
26+
case .failure(let error): promise.fail(error)
27+
}
28+
}
29+
return promise.futureResult
30+
}
31+
32+
// MARK: - flatMapAlways
33+
34+
/// When the current `EventLoopFuture` receives any result, run the provided callback, which will provide a new
35+
/// `EventLoopFuture`. Essentially combines the behaviors of `.always(_:)` and `.flatMap(file:line:_:)`.
36+
///
37+
/// This is useful when some work must be done for both success and failure states, especially work that requires
38+
/// temporarily extending the lifetime of one or more objects.
39+
public func flatMapAlways<NewValue>(
40+
file: StaticString = #file, line: UInt = #line,
41+
_ callback: @escaping (Result<Value, Error>) -> EventLoopFuture<NewValue>
42+
) -> EventLoopFuture<NewValue> {
43+
let promise = self.eventLoop.makePromise(of: NewValue.self, file: file, line: line)
44+
self.whenComplete { result in callback(result).cascade(to: promise) }
45+
return promise.futureResult
46+
}
47+
}

Sources/AsyncKit/EventLoopFuture/FutureExtensions.swift renamed to Sources/AsyncKit/EventLoopFuture/Future+Nonempty.swift

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,4 @@
11
extension EventLoopFuture {
2-
// MARK: - Guard
3-
4-
/// Guards that the future's value satisfies the callback's condition or
5-
/// fails with the given error.
6-
///
7-
///
8-
/// Example usage:
9-
///
10-
/// future.guard({ $0.userID == user.id }, else: AccessError.unauthorized)
11-
///
12-
/// - parameters:
13-
/// - callback: Callback that asynchronously executes a condition.
14-
/// - error: The error to fail with if condition isn't met.
15-
/// - returns: A future containing the original future's result.
16-
public func `guard`(_ callback: @escaping ((Value) -> Bool), else error: @escaping @autoclosure () -> Error) -> EventLoopFuture<Value> {
17-
let promise = self.eventLoop.makePromise(of: Value.self)
18-
self.whenComplete { result in
19-
switch result {
20-
case .success(let value):
21-
if callback(value) {
22-
promise.succeed(value)
23-
} else {
24-
promise.fail(error())
25-
}
26-
case .failure(let error): promise.fail(error)
27-
}
28-
}
29-
return promise.futureResult
30-
}
31-
32-
// MARK: - flatMapAlways
33-
34-
/// When the current `EventLoopFuture` receives any result, run the provided callback, which will provide a new
35-
/// `EventLoopFuture`. Essentially combines the behaviors of `.always(_:)` and `.flatMap(file:line:_:)`.
36-
///
37-
/// This is useful when some work must be done for both success and failure states, especially work that requires
38-
/// temporarily extending the lifetime of one or more objects.
39-
public func flatMapAlways<NewValue>(
40-
file: StaticString = #file, line: UInt = #line,
41-
_ callback: @escaping (Result<Value, Error>) -> EventLoopFuture<NewValue>
42-
) -> EventLoopFuture<NewValue> {
43-
let promise = self.eventLoop.makePromise(of: NewValue.self, file: file, line: line)
44-
self.whenComplete { result in callback(result).cascade(to: promise) }
45-
return promise.futureResult
46-
}
47-
48-
// MARK: - nonempty
49-
502
/// Checks that the future's value (if any) returns `false` for `.isEmpty`. If the check fails, the provided error
513
/// is thrown.
524
public func nonempty<E: Error>(orError error: @escaping @autoclosure () -> E) -> EventLoopFuture<Value> where Value: Collection {
@@ -116,5 +68,4 @@ extension EventLoopFuture {
11668
) -> EventLoopFuture<NewValue> where Value: Collection, NewValue: RangeReplaceableCollection {
11769
return self.nonemptyFlatMap(or: .init(), transform)
11870
}
119-
12071
}

Sources/AsyncKit/EventLoopFuture/Future+Try.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ extension EventLoopFuture {
1313
///
1414
/// With `tryFlatMap`, the provided callback _may_ throw Errors, causing the returned `EventLoopFuture<Value>`
1515
/// to report failure immediately after the completion of the original `EventLoopFuture`.
16-
flatMap(file: file, line: line) { [eventLoop] value in
16+
return self.flatMap(file: file, line: line) { [eventLoop] value in
1717
do {
1818
return try callback(value)
1919
} catch {

Tests/AsyncKitTests/AsyncKitTests.swift

Lines changed: 0 additions & 52 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import XCTest
2+
import AsyncKit
3+
import Logging
4+
5+
enum TestError: Error {
6+
case generic
7+
case notEqualTo1
8+
case notEqualToBar
9+
}
10+
11+
func env(_ name: String) -> String? {
12+
return ProcessInfo.processInfo.environment[name]
13+
}
14+
15+
let isLoggingConfigured: Bool = {
16+
LoggingSystem.bootstrap { label in
17+
var handler = StreamLogHandler.standardOutput(label: label)
18+
handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .debug
19+
return handler
20+
}
21+
return true
22+
}()

0 commit comments

Comments
 (0)