A deadline algorithm for Swift Concurrency.
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.
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.
To fully understand this, let's illustrate the 3 outcomes of this function:
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
.
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.
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)
.
- Only have one free function with a default expression of
ContinuousClock
for theclock
parameter.- Blocked by: swiftlang/swift#72199
- Use
sending
instead of@Sendable
for region based isolation support.- Blocked by: swiftlang/swift#76242
- Use
@isolated(any)
for synchronous task enqueueing support.- Blocked by: swiftlang/swift#76604