Skip to content

Commit 3a186ea

Browse files
authored
Add withTaskCancellationOrGracefulShutdownHandler (#176)
* Add withTaskCancellationOrGracefulShutdownHandler * Test withTaskCancellationOrGracefulShutdownHandler
1 parent 55f45e3 commit 3a186ea

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

Sources/ServiceLifecycle/GracefulShutdown.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,39 @@ public func withGracefulShutdownHandler<T>(
5555
return try await operation()
5656
}
5757

58+
/// Execute an operation with a graceful shutdown or task cancellation handler that’s immediately invoked if the current task is
59+
/// shutting down gracefully or has been cancelled.
60+
///
61+
/// This doesn’t check for graceful shutdown, and always executes the passed operation.
62+
/// The operation executes on the calling execution context and does not suspend by itself, unless the code contained within the closure does.
63+
/// If graceful shutdown or task cancellation occurs while the operation is running, the cancellation/graceful shutdown handler will execute
64+
/// concurrently with the operation.
65+
///
66+
/// When `withTaskCancellationOrGracefulShutdownHandler` is used in a Task that has already been gracefully shutdown or cancelled, the
67+
/// `onCancelOrGracefulShutdown` handler will be executed immediately before operation gets to execute. This allows the `onCancelOrGracefulShutdown`
68+
/// handler to set some external “shutdown” flag that the operation may be atomically checking for in order to avoid performing any actual work
69+
/// once the operation gets to run.
70+
///
71+
/// - Important: This method will only set up a graceful shutdown handler if run inside ``ServiceGroup`` otherwise no graceful shutdown handler
72+
/// will be set up.
73+
///
74+
/// - Parameters:
75+
/// - operation: The actual operation.
76+
/// - handler: The handler which is invoked once graceful shutdown or task cancellation has been triggered.
77+
// Unsafely inheriting the executor is safe to do here since we are not calling any other async method
78+
// except the operation. This makes sure no other executor hops would occur here.
79+
@_unsafeInheritExecutor
80+
public func withTaskCancellationOrGracefulShutdownHandler<T>(
81+
operation: () async throws -> T,
82+
onCancelOrGracefulShutdown handler: @Sendable @escaping () -> Void
83+
) async rethrows -> T {
84+
return try await withTaskCancellationHandler {
85+
try await withGracefulShutdownHandler(operation: operation, onGracefulShutdown: handler)
86+
} onCancel: {
87+
handler()
88+
}
89+
}
90+
5891
/// Waits until graceful shutdown is triggered.
5992
///
6093
/// This method suspends the caller until graceful shutdown is triggered. If the calling task is cancelled before

Tests/ServiceLifecycleTests/GracefulShutdownTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,54 @@ final class GracefulShutdownTests: XCTestCase {
303303
XCTAssertTrue(error is CancellationError)
304304
}
305305
}
306+
307+
func testWithTaskCancellationOrGracefulShutdownHandler_GracefulShutdown() async throws {
308+
var cont: AsyncStream<Void>.Continuation!
309+
let stream = AsyncStream<Void> { cont = $0 }
310+
let continuation = cont!
311+
312+
await testGracefulShutdown { gracefulShutdownTestTrigger in
313+
await withTaskCancellationOrGracefulShutdownHandler {
314+
await withTaskGroup(of: Void.self) { group in
315+
group.addTask {
316+
await stream.first { _ in true }
317+
}
318+
319+
gracefulShutdownTestTrigger.triggerGracefulShutdown()
320+
321+
await group.waitForAll()
322+
}
323+
} onCancelOrGracefulShutdown: {
324+
continuation.finish()
325+
}
326+
}
327+
}
328+
329+
func testWithTaskCancellationOrGracefulShutdownHandler_TaskCancellation() async throws {
330+
var cont: AsyncStream<Void>.Continuation!
331+
let stream = AsyncStream<Void> { cont = $0 }
332+
let continuation = cont!
333+
334+
await withTaskGroup(of: Void.self) { group in
335+
group.addTask {
336+
var cancelCont: AsyncStream<CheckedContinuation<Void, Never>>.Continuation!
337+
let cancelStream = AsyncStream<CheckedContinuation<Void, Never>> { cancelCont = $0 }
338+
let cancelContinuation = cancelCont!
339+
await withTaskCancellationOrGracefulShutdownHandler {
340+
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
341+
cancelContinuation.yield(cont)
342+
continuation.finish()
343+
}
344+
} onCancelOrGracefulShutdown: {
345+
Task {
346+
let cont = await cancelStream.first(where: { _ in true })!
347+
cont.resume()
348+
}
349+
}
350+
}
351+
// wait for task to startup
352+
_ = await stream.first { _ in true }
353+
group.cancelAll()
354+
}
355+
}
306356
}

0 commit comments

Comments
 (0)