From e5ba7d87284da64ec5a1216958c91df61854bba6 Mon Sep 17 00:00:00 2001 From: Jakub Skotnicki <1700160+Skoti@users.noreply.github.com> Date: Wed, 5 Mar 2025 19:59:41 +0100 Subject: [PATCH] add race and timeout/deadline async functions --- Sources/AsyncAlgorithms/AsyncRace.swift | 57 ++++++++++++ Sources/AsyncAlgorithms/AsyncTimeout.swift | 101 +++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 Sources/AsyncAlgorithms/AsyncRace.swift create mode 100644 Sources/AsyncAlgorithms/AsyncTimeout.swift diff --git a/Sources/AsyncAlgorithms/AsyncRace.swift b/Sources/AsyncAlgorithms/AsyncRace.swift new file mode 100644 index 00000000..7a3bd524 --- /dev/null +++ b/Sources/AsyncAlgorithms/AsyncRace.swift @@ -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(_ 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(_ 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() + } + return try await group.next() + } +} + +/// Returns the value or throws an error, from the first completed or failed operation. +public func race(_ 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(_ 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 + } + } +} diff --git a/Sources/AsyncAlgorithms/AsyncTimeout.swift b/Sources/AsyncAlgorithms/AsyncTimeout.swift new file mode 100644 index 00000000..7c07363c --- /dev/null +++ b/Sources/AsyncAlgorithms/AsyncTimeout.swift @@ -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( + _ 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( + _ 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( + _ duration: Duration, + tolerance: Duration? = nil, + clock: any Clock, + 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( + 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( + 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 + } +}