Skip to content

Commit 6028256

Browse files
committed
Initial Commit
1 parent 241cb08 commit 6028256

File tree

4 files changed

+149
-0
lines changed

4 files changed

+149
-0
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

Package.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// swift-tools-version: 5.10
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "swift-concurrency-deadline",
7+
platforms: [.iOS(.v16), .macOS(.v13), .macCatalyst(.v16), .tvOS(.v16), .watchOS(.v9), .visionOS(.v1)],
8+
products: [
9+
.library(name: "Deadline", targets: ["Deadline"]),
10+
],
11+
targets: [
12+
.target(
13+
name: "Deadline",
14+
swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]
15+
),
16+
.testTarget(
17+
name: "DeadlineTests",
18+
dependencies: ["Deadline"],
19+
swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]
20+
),
21+
]
22+
)

Sources/Deadline/Deadline.swift

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
enum DeadlineState<T> {
2+
case result(Result<T, any Error>)
3+
case sleepWasCancelled
4+
case deadlineExceeded
5+
}
6+
7+
/// An error indicating that the deadline has passed and the operation did not complete.
8+
public struct DeadlineExceededError: Error { }
9+
10+
/// Race the given operation against a deadline.
11+
///
12+
/// This is a helper that you can use for asynchronous APIs that do not support timeouts/deadlines natively.
13+
/// It will start a `TaskGroup` with two child tasks: your operation and a `Task.sleep(until:tolerance:clock:)`. There are three possible outcomes:
14+
/// 1. If your operation finishes first, it will simply return the result and cancel the sleeping.
15+
/// 2. If the sleeping finishes first, it will throw a `DeadlineExceededError` and cancel your operation.
16+
/// 3. If the parent task was cancelled, it will automatically cancel your operation and the sleeping. The cancellation handling will be inferred from your operation. `CancellationError`s from `Task.sleep(until:tolerance:clock:)` will be ignored.
17+
/// - Important: The operation closure must support cooperative cancellation.
18+
/// Otherwise, `withDeadline(until:tolerance:clock:operation:)` will suspend execution until the operation completes, making the deadline ineffective.
19+
/// ## Example
20+
/// This is just a demonstrative usage of this function. `CBCentralManager.connect(_:)` is a good example, in my opinion, since it does not support timeouts natively.
21+
///
22+
/// Again, if you try to make something like `CBCentralManager.connect(_:)` asynchronous and use it with `withDeadline(until:tolerance:clock:operation:)` be sure to use `withTaskCancellationHandler(operation:onCancel:)` at some point to opt into cooperative cancellation.
23+
/// ```swift
24+
/// try await withDeadline(until: .now + seconds(5), clock: .continous) {
25+
/// try await cbCentralManager.connect(peripheral)
26+
/// }
27+
/// ```
28+
public func withDeadline<C, T>(
29+
until instant: C.Instant,
30+
tolerance: C.Instant.Duration? = nil,
31+
clock: C,
32+
operation: @escaping @Sendable () async throws -> T
33+
) async throws -> T where C: Clock, T: Sendable {
34+
35+
let result = await withTaskGroup(
36+
of: DeadlineState<T>.self,
37+
returning: Result<T, any Error>.self
38+
) { taskGroup in
39+
40+
taskGroup.addTask {
41+
do {
42+
return try await .result(.success(operation()))
43+
} catch {
44+
return .result(.failure(error))
45+
}
46+
}
47+
48+
taskGroup.addTask {
49+
do {
50+
try await Task.sleep(until: instant, tolerance: tolerance, clock: clock)
51+
return .deadlineExceeded
52+
} catch {
53+
return .sleepWasCancelled
54+
}
55+
}
56+
57+
// Make sure to cancel the remaining child task.
58+
defer {
59+
taskGroup.cancelAll()
60+
}
61+
62+
for await next in taskGroup {
63+
switch next {
64+
// This indicates that the operation did complete. We can safely return the result.
65+
case let .result(result):
66+
return result
67+
// This indicates that the operation did not complete in time. We will throw `DeadlineExceededError`.
68+
case .deadlineExceeded:
69+
return .failure(DeadlineExceededError())
70+
// This indicates that the sleep child task was the first to return.
71+
// However we want to keep the cancellation handling of the operation. Therefore we will skip this iteration and wait for the operation child tasks result.
72+
case .sleepWasCancelled:
73+
continue
74+
}
75+
}
76+
77+
preconditionFailure("Invalid state")
78+
}
79+
80+
return try result.get()
81+
}
82+
83+
/// Race the given operation against a deadline.
84+
///
85+
/// This is a helper that you can use for asynchronous APIs that do not support timeouts/deadlines natively.
86+
/// It will start a `TaskGroup` with two child tasks: your operation and a `Task.sleep(until:tolerance:clock:)`. There are three possible outcomes:
87+
/// 1. If your operation finishes first, it will simply return the result and cancel the sleeping.
88+
/// 2. If the sleeping finishes first, it will throw a `DeadlineExceededError` and cancel your operation.
89+
/// 3. If the parent task was cancelled, it will automatically cancel your operation and the sleeping. The cancellation handling will be inferred from your operation. `CancellationError`s from `Task.sleep(until:tolerance:clock:)` will be ignored.
90+
/// - Important: The operation closure must support cooperative cancellation.
91+
/// Otherwise, `withDeadline(until:tolerance:operation:)` will suspend execution until the operation completes, making the deadline ineffective.
92+
/// ## Example
93+
/// This is just a demonstrative usage of this function. `CBCentralManager.connect(_:)` is a good example, in my opinion, since it does not support timeouts natively.
94+
///
95+
/// Again, if you try to make something like `CBCentralManager.connect(_:)` asynchronous and use it with `withDeadline(until:tolerance:operation:)` be sure to use `withTaskCancellationHandler(operation:onCancel:)` at some point to opt into cooperative cancellation.
96+
/// ```swift
97+
/// try await withDeadline(until: .now + seconds(5), clock: .continous) {
98+
/// try await cbCentralManager.connect(peripheral)
99+
/// }
100+
/// ```
101+
public func withDeadline<T>(
102+
until instant: ContinuousClock.Instant,
103+
tolerance: ContinuousClock.Instant.Duration? = nil,
104+
operation: @escaping @Sendable () async throws -> T
105+
) async throws -> T where T: Sendable {
106+
try await withDeadline(until: instant, tolerance: tolerance, clock: ContinuousClock(), operation: operation)
107+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import XCTest
2+
@testable import swift_concurrency_deadline
3+
4+
final class swift_concurrency_deadlineTests: XCTestCase {
5+
func testExample() throws {
6+
// XCTest Documentation
7+
// https://developer.apple.com/documentation/xctest
8+
9+
// Defining Test Cases and Test Methods
10+
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11+
}
12+
}

0 commit comments

Comments
 (0)