ph1ps/swift-concurrency-deadline
A deadline algorithm for Swift Concurrency.
Rationale
As I've previously stated on the Swift forums: in my opinion deadlines or timeouts are a missing piece in Swift's Concurrency system. Since this algorithm is not easy to get right I decided to open-source my implementation.
Details
The library comes with two free functions, one with a generic clock and another one which uses the ContinuousClock as default.
public func deadline<C, R>(
until instant: C.Instant,
tolerance: C.Instant.Duration? = nil,
clock: C,
isolation: isolated (any Actor)? = #isolation,
operation: @Sendable () async throws -> R
) async throws -> R where C: Clock, R: Sendable { ... }
public func deadline<R>(
until instant: ContinuousClock.Instant,
tolerance: ContinuousClock.Instant.Duration? = nil,
isolation: isolated (any Actor)? = #isolation,
operation: @Sendable () async throws -> R
) async throws -> R where R: Sendable { ... }This function provides a mechanism for enforcing timeouts on asynchronous operations that lack native deadline support. It creates a TaskGroup with two concurrent tasks: the provided operation and a sleep task.
- Parameters:
- instant: The absolute deadline for the operation to complete. - tolerance: The allowed tolerance for the deadline. - clock: The clock used for timing the operation. - isolation: The isolation passed on to the task group. - operation: The asynchronous operation to be executed.
- Returns: The result of the operation if it completes before the deadline.
- Throws:
DeadlineExceededError, if the operation fails to complete before the deadline and errors thrown by the operation or clock.
[!CAUTION] The operation closure must support cooperative cancellation. Otherwise, the deadline will not be respected.
Examples
To fully understand this, let's illustrate the 3 outcomes of this function:
Outcome 1
The operation finishes in time:
let result = try await deadline(until: .now + .seconds(5)) {
// Simulate long running task
try await Task.sleep(for: .seconds(1))
return "success"
}As you'd expect, result will be "success". The same applies when your operation fails in time:
let result = try await deadline(until: .now + .seconds(5)) {
// Simulate long running task
try await Task.sleep(for: .seconds(1))
throw CustomError()
}This will throw CustomError.
Outcome 2
The operation does not finish in time:
let result = try await deadline(until: .now + .seconds(1)) {
// Simulate even longer running task
try await Task.sleep(for: .seconds(5))
return "success"
}This will throw DeadlineExceededError because the operation will not finish in time.
Outcome 3
The parent task was cancelled:
let task = Task {
do {
try await deadline(until: .now + .seconds(5)) {
try await URLSession.shared.data(from: url)
}
} catch {
print(error)
}
}
task.cancel()The print is guaranteed to print URLError(.cancelled).
Improvements
- Only have one free function with a default expression of
ContinuousClockfor theclockparameter.
- Blocked by: https://github.com/swiftlang/swift/issues/72199
- Use
@isolated(any)for synchronous task enqueueing support.
- Blocked by: https://github.com/swiftlang/swift/issues/76604
Package Metadata
Repository: ph1ps/swift-concurrency-deadline
Default branch: main
README: README.md