Skip to content

Commit 81a9a28

Browse files
committed
withAsyncTaskCleanupHandler calls its cleanup handler twice if body throws
If the body closure given to withAsyncTaskCleanupHandler throws, its cleanup handler is called twice: once in the catch block where body is called, and then again in the task group task once the Task.sleep throws due to cancellation, which swallows the error and then continues to call the handler as well. That results in the teardown sequence being invoked twice, as well as the teardown sequence being invoked for non-error cases. This patch ensures the cleanup handler is invoked in failure cases only, and only once. Closes #80
1 parent ca05b23 commit 81a9a28

File tree

1 file changed

+28
-3
lines changed

1 file changed

+28
-3
lines changed

Sources/Subprocess/Configuration.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -752,11 +752,13 @@ extension Optional where Wrapped == String {
752752
}
753753
}
754754

755+
/// Runs `body`, and then runs `onCleanup` if body throws an error, or if the parent task is cancelled. In the latter case, `onCleanup` may be run concurrently with `body`. `body` is guaranteed to run exactly once. `onCleanup` is guaranteed to run only once, or not at all.
755756
internal func withAsyncTaskCleanupHandler<Result>(
756757
_ body: () async throws -> Result,
757758
onCleanup handler: @Sendable @escaping () async -> Void,
758759
isolation: isolated (any Actor)? = #isolation
759760
) async rethrows -> Result {
761+
let (runCancellationHandlerStream, runCancellationHandlerContinuation) = AsyncThrowingStream.makeStream(of: Void.self)
760762
return try await withThrowingTaskGroup(
761763
of: Void.self,
762764
returning: Result.self
@@ -767,15 +769,38 @@ internal func withAsyncTaskCleanupHandler<Result>(
767769
// before the time ends. We then run the cancel handler.
768770
do { while true { try await Task.sleep(nanoseconds: 1_000_000_000) } } catch {}
769771
// Run task cancel handler
770-
await handler()
772+
runCancellationHandlerContinuation.finish(throwing: CancellationError())
773+
}
774+
775+
group.addTask {
776+
// Enumerate the async stream until it completes or throws an error.
777+
// Since we signal completion of the stream from cancellation or the
778+
// parent task or the body throwing, this ensures that we run the
779+
// cleanup handler exactly once in any failure scenario, and also do
780+
// so _immediately_ if the failure scenario is due to parent task
781+
// cancellation. We do so in a detached Task to prevent cancellation
782+
// of the parent task from interrupting enumeration of the stream itself.
783+
await Task.detached {
784+
do {
785+
var iterator = runCancellationHandlerStream.makeAsyncIterator()
786+
while let _ = try await iterator.next() {
787+
}
788+
} catch {
789+
await handler()
790+
}
791+
}.value
792+
}
793+
794+
defer {
795+
group.cancelAll()
771796
}
772797

773798
do {
774799
let result = try await body()
775-
group.cancelAll()
800+
runCancellationHandlerContinuation.finish()
776801
return result
777802
} catch {
778-
await handler()
803+
runCancellationHandlerContinuation.finish(throwing: error)
779804
throw error
780805
}
781806
}

0 commit comments

Comments
 (0)