-
Notifications
You must be signed in to change notification settings - Fork 179
Add race, timeout, and deadline async functions #343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift Async Algorithms open source project | ||
// | ||
// Copyright (c) 2025 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
/// Returns the value or throws an error, from the first completed or failed operation. | ||
public func race(_ operations: (@Sendable () async throws -> Void)...) async throws { | ||
try await race(operations) | ||
} | ||
|
||
/// Returns the value or throws an error, from the first completed or failed operation. | ||
public func race<T: Sendable>(_ operations: (@Sendable () async throws -> T)...) async throws -> T? { | ||
try await race(operations) | ||
} | ||
|
||
/// Returns the value or throws an error, from the first completed or failed operation. | ||
public func race<T: Sendable>(_ operations: [@Sendable () async throws -> T]) async throws -> T? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using an array here we might be better off making this generic by doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I cannot use If I do this:
Sending a sequence of closures doesn't imply closures as sending, which makes total sense, this information would need to be passed through generics. Would this require to have |
||
try await withThrowingTaskGroup(of: T.self) { group in | ||
operations.forEach { operation in | ||
group.addTask { try await operation() } | ||
} | ||
defer { | ||
group.cancelAll() | ||
} | ||
return try await group.next() | ||
} | ||
} | ||
|
||
/// Returns the value or throws an error, from the first completed or failed operation. | ||
public func race<T: Sendable>(_ operations: (@Sendable () async throws -> T?)...) async throws -> T? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need optional overloads here at all There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do need them. Otherwise, if |
||
try await race(operations) | ||
} | ||
|
||
/// Returns the value or throws an error, from the first completed or failed operation. | ||
public func race<T: Sendable>(_ operations: [@Sendable () async throws -> T?]) async throws -> T? { | ||
try await withThrowingTaskGroup(of: T?.self) { group in | ||
operations.forEach { operation in | ||
group.addTask { try await operation() } | ||
} | ||
defer { | ||
group.cancelAll() | ||
} | ||
let value = try await group.next() | ||
switch value { | ||
case .none: | ||
return nil | ||
case let .some(value): | ||
return value | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift Async Algorithms open source project | ||
// | ||
// Copyright (c) 2025 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
/** | ||
- Parameters: | ||
- customError: The failure returned by this closure is thrown when the operation timeouts. | ||
If `customError` is `nil`, then `CancellationError` is thrown. | ||
*/ | ||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) | ||
public func withTimeout<Success: Sendable>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that's the shape of how this method should look like. Ideally we end up with something closer to this:
Importantly, here the body closure is both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
_ duration: ContinuousClock.Duration, | ||
tolerance: ContinuousClock.Duration? = nil, | ||
customError: (@Sendable () -> Error)? = nil, | ||
operation: @Sendable () async throws -> Success | ||
) async throws -> Success { | ||
let clock = ContinuousClock() | ||
return try await withDeadline(after: clock.now.advanced(by: duration), tolerance: tolerance, clock: clock, customError: customError, operation: operation) | ||
} | ||
|
||
#if compiler(<6.1) | ||
/** | ||
- Parameters: | ||
- customError: The failure returned by this closure is thrown when the operation timeouts. | ||
If `customError` is `nil`, then `CancellationError` is thrown. | ||
*/ | ||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) | ||
public func withTimeout<C: Clock, Success: Sendable>( | ||
_ duration: C.Duration, | ||
tolerance: C.Duration? = nil, | ||
clock: C, | ||
customError: (@Sendable () -> Error)? = nil, | ||
operation: @Sendable () async throws -> Success | ||
) async throws -> Success { | ||
try await withDeadline(after: clock.now.advanced(by: duration), tolerance: tolerance, clock: clock, customError: customError, operation: operation) | ||
} | ||
#endif | ||
|
||
/** | ||
- Parameters: | ||
- customError: The failure returned by this closure is thrown when the operation timeouts. | ||
If `customError` is `nil`, then `CancellationError` is thrown. | ||
*/ | ||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) | ||
public func withTimeout<Success: Sendable>( | ||
_ duration: Duration, | ||
tolerance: Duration? = nil, | ||
clock: any Clock<Duration>, | ||
customError: (@Sendable () -> Error)? = nil, | ||
operation: @Sendable () async throws -> Success | ||
) async throws -> Success { | ||
try await withoutActuallyEscaping(operation) { operation in | ||
try await race(operation) { | ||
try await clock.sleep(for: duration, tolerance: tolerance) | ||
throw customError?() ?? CancellationError() | ||
}.unsafelyUnwrapped | ||
} | ||
} | ||
|
||
/** | ||
- Parameters: | ||
- customError: The failure returned by this closure is thrown when the operation timeouts. | ||
If `customError` is `nil`, then `CancellationError` is thrown. | ||
*/ | ||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) | ||
public func withDeadline<Success: Sendable>( | ||
after instant: ContinuousClock.Instant, | ||
tolerance: ContinuousClock.Duration? = nil, | ||
customError: (@Sendable () -> Error)? = nil, | ||
operation: @Sendable () async throws -> Success | ||
) async throws -> Success { | ||
try await withDeadline(after: instant, tolerance: tolerance, clock: .continuous, customError: customError, operation: operation) | ||
} | ||
|
||
/** | ||
- Parameters: | ||
- customError: The failure returned by this closure is thrown when the operation timeouts. | ||
If `customError` is `nil`, then `CancellationError` is thrown. | ||
*/ | ||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) | ||
public func withDeadline<C: Clock, Success: Sendable>( | ||
after instant: C.Instant, | ||
tolerance: C.Duration? = nil, | ||
clock: C, | ||
customError: (@Sendable () -> Error)? = nil, | ||
operation: @Sendable () async throws -> Success | ||
) async throws -> Success { | ||
try await withoutActuallyEscaping(operation) { operation in | ||
try await race(operation) { | ||
try await clock.sleep(until: instant, tolerance: tolerance) | ||
throw customError?() ?? CancellationError() | ||
}.unsafelyUnwrapped | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All those closure's can be
sending
since we move them into a child task. Also do we really need these overloads?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding
Void
overloads. If you do this:You get:
Interestingly, if you don't use the
return
keyword and rely on inference, this compiles 😄