File tree 2 files changed +38
-0
lines changed
2 files changed +38
-0
lines changed Original file line number Diff line number Diff line change @@ -40,10 +40,21 @@ extension Task where Success == Never, Failure == Never {
40
40
41
41
try await withTaskCancellationHandler {
42
42
try await withCheckedThrowingContinuation { continuation in
43
+ // The `continuation` handler should never be `resume`'d more than once.
44
+ // Setting the eventHandler on the timer after it has been cancelled will be ignored.
45
+ // https://github.com/apple/swift-corelibs-libdispatch/blob/77766345740dfe3075f2f60bead854b29b0cfa24/src/source.c#L338
46
+ // Therefore, set a flag indicating `resume` has already been called to avoid `resume`ing more than once.
47
+ // Cancelling the timer does not cancel an event handler that is already running however,
48
+ // the cancel handler will be scheduled after the event handler has finished.
49
+ // https://developer.apple.com/documentation/dispatch/1385604-dispatch_source_cancel
50
+ // Therefore, there it is safe to do this here.
51
+ var hasResumed = false
43
52
timer. setEventHandler {
53
+ hasResumed = true
44
54
continuation. resume ( )
45
55
}
46
56
timer. setCancelHandler {
57
+ guard hasResumed == false else { return }
47
58
continuation. resume ( throwing: TaskCancellationError ( ) )
48
59
}
49
60
timer. schedule ( wallDeadline: . now( ) + DispatchTimeInterval. milliseconds ( duration. milliseconds) )
Original file line number Diff line number Diff line change @@ -25,4 +25,31 @@ final class TaskSleepTests: XCTestCase {
25
25
XCTAssert ( error is CancellationError )
26
26
}
27
27
}
28
+
29
+ /// This test triggers a race condition in `sleepUsingContinuousClock` where an `AutoCancellingTask` will
30
+ /// cancel a `DispatchSourceTimer` in a `Task` trying to call `resume` on its continuation handler more than once
31
+ func testSuccessfulEventHandlerRemovesCancellation( ) async throws {
32
+ for _ in 0 ... 20 {
33
+ let task = recoveryTask ( )
34
+ try await Task . sleep ( duration: . milliseconds( 10 ) )
35
+ task. callDummyFunctionToForceConcurrencyWait ( )
36
+ }
37
+ }
38
+
39
+ private func recoveryTask( ) -> AutoCancellingTask {
40
+ AutoCancellingTask ( Task . detached {
41
+ while Task . isCancelled == false {
42
+ try await Task . sleepUsingContinuousClock ( for: . milliseconds( 10 ) )
43
+ }
44
+ } )
45
+ }
46
+ }
47
+
48
+ private extension AutoCancellingTask {
49
+ /// This function is here to silence a warning about unused variables in `testSuccessfulEventHandlerRemovesCancellation`
50
+ /// The following construct `_ = recoveryTask()` cannot be used as the resulting `AutoCancellingTask`
51
+ /// would immediately get `deinit`ed, changing the test scenario.
52
+ /// A dummy function is needed to make sure the task is not cancelled before concurrency is forced
53
+ /// by having a call to `Task.sleep`
54
+ func callDummyFunctionToForceConcurrencyWait( ) { }
28
55
}
You can’t perform that action at this time.
0 commit comments