Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Sources/AsyncAlgorithms/AsyncRace.swift
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 {
Copy link
Member

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?

Copy link
Author

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:

    let u: () async throws -> Void = {
        return try await race {
            let _ = 2
        }
    }

You get:

Cannot convert value of type '()?' to closure result type 'Void'

Interestingly, if you don't use the return keyword and rely on inference, this compiles 😄

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? {
Copy link
Member

Choose a reason for hiding this comment

The 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 some Sequence<sending () asynchronous throws -> T>

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot use sending for the closure - this gives an error: sending' may only be used on parameters and results.
The same for the comment above regarding the variadic parameter.

If I do this:
_ operations: sending some Sequence<() async throws -> T>
then the compiler gives a warning at:
group.addTask { try await operation() }
saying:

Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure; this is an error in the Swift 6 language mode

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 @Sending to be added to the language?

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? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need optional overloads here at all

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do need them. Otherwise, if operations return an optional type, you end up with double optional T?? due to the race function returning the result of try await group.next(), which is an optional. So these overloads are here to flatten this conveniently.

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
}
}
}
101 changes: 101 additions & 0 deletions Sources/AsyncAlgorithms/AsyncTimeout.swift
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>(
Copy link
Member

Choose a reason for hiding this comment

The 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:

nonisolated(nonsending) public func withTimeout<T: Sendable, Clock: _Concurrency.Clock>(
    in timeout: Clock.Duration,
    clock: Clock,
    body: nonisolated(nonsending) () async throws -> T
) async throws -> T

Importantly, here the body closure is both non-Sendable and non-escaping. This allows to easily compose it with surrounding context.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nonisolated(nonsending) is a Swift 6.2 feature. This package supports Swift 5.8. Do we want these methods to be 6.2+ only or define a different API for previous Swift versions?

_ 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
}
}