Skip to content

Commit d7fe0e7

Browse files
authored
cancelOnGracefulShutdown hangs, if cancellation is not immediately (#177)
* Add test case * Fix tests * Make code simpler * Fix Sendable * swift-format
1 parent 3a186ea commit d7fe0e7

File tree

4 files changed

+118
-24
lines changed

4 files changed

+118
-24
lines changed

Sources/ServiceLifecycle/AsyncGracefulShutdownSequence.swift

+3-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
@usableFromInline
2121
struct AsyncGracefulShutdownSequence: AsyncSequence, Sendable {
2222
@usableFromInline
23-
typealias Element = Void
23+
typealias Element = CancellationWaiter.Reason
2424

2525
@inlinable
2626
init() {}
@@ -36,9 +36,8 @@ struct AsyncGracefulShutdownSequence: AsyncSequence, Sendable {
3636
init() {}
3737

3838
@inlinable
39-
func next() async throws -> Element? {
40-
try await CancellationWaiter().wait()
41-
return ()
39+
func next() async -> Element? {
40+
await CancellationWaiter().wait()
4241
}
4342
}
4443
}

Sources/ServiceLifecycle/CancellationWaiter.swift

+15-13
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,38 @@
1515
/// An actor that provides a function to wait on cancellation/graceful shutdown.
1616
@usableFromInline
1717
actor CancellationWaiter {
18-
private var taskContinuation: CheckedContinuation<Void, Error>?
18+
@usableFromInline
19+
enum Reason: Sendable {
20+
case cancelled
21+
case gracefulShutdown
22+
}
23+
24+
private var taskContinuation: CheckedContinuation<Reason, Never>?
1925

2026
@usableFromInline
2127
init() {}
2228

2329
@usableFromInline
24-
func wait() async throws {
25-
try await withTaskCancellationHandler {
26-
try await withGracefulShutdownHandler {
27-
try await withCheckedThrowingContinuation { continuation in
30+
func wait() async -> Reason {
31+
await withTaskCancellationHandler {
32+
await withGracefulShutdownHandler {
33+
await withCheckedContinuation { (continuation: CheckedContinuation<Reason, Never>) in
2834
self.taskContinuation = continuation
2935
}
3036
} onGracefulShutdown: {
3137
Task {
32-
await self.finish()
38+
await self.finish(reason: .gracefulShutdown)
3339
}
3440
}
3541
} onCancel: {
3642
Task {
37-
await self.finish(throwing: CancellationError())
43+
await self.finish(reason: .cancelled)
3844
}
3945
}
4046
}
4147

42-
private func finish(throwing error: Error? = nil) {
43-
if let error {
44-
self.taskContinuation?.resume(throwing: error)
45-
} else {
46-
self.taskContinuation?.resume()
47-
}
48+
private func finish(reason: Reason) {
49+
self.taskContinuation?.resume(returning: reason)
4850
self.taskContinuation = nil
4951
}
5052
}

Sources/ServiceLifecycle/GracefulShutdown.swift

+16-7
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,21 @@ public func withTaskCancellationOrGracefulShutdownHandler<T>(
9595
///
9696
/// - Throws: `CancellationError` if the task is cancelled.
9797
public func gracefulShutdown() async throws {
98-
try await AsyncGracefulShutdownSequence().first { _ in true }
98+
switch await AsyncGracefulShutdownSequence().first(where: { _ in true }) {
99+
case .cancelled:
100+
throw CancellationError()
101+
case .gracefulShutdown:
102+
return
103+
case .none:
104+
fatalError()
105+
}
99106
}
100107

101108
/// This is just a helper type for the result of our task group.
102109
enum ValueOrGracefulShutdown<T: Sendable>: Sendable {
103110
case value(T)
104111
case gracefulShutdown
112+
case cancelled
105113
}
106114

107115
/// Cancels the closure when a graceful shutdown was triggered.
@@ -115,11 +123,12 @@ public func cancelOnGracefulShutdown<T: Sendable>(_ operation: @Sendable @escapi
115123
}
116124

117125
group.addTask {
118-
for try await _ in AsyncGracefulShutdownSequence() {
126+
switch await CancellationWaiter().wait() {
127+
case .cancelled:
128+
return .cancelled
129+
case .gracefulShutdown:
119130
return .gracefulShutdown
120131
}
121-
122-
throw CancellationError()
123132
}
124133

125134
let result = try await group.next()
@@ -128,13 +137,13 @@ public func cancelOnGracefulShutdown<T: Sendable>(_ operation: @Sendable @escapi
128137
switch result {
129138
case .value(let t):
130139
return t
131-
case .gracefulShutdown:
140+
141+
case .gracefulShutdown, .cancelled:
132142
switch try await group.next() {
133143
case .value(let t):
134144
return t
135-
case .gracefulShutdown:
145+
case .gracefulShutdown, .cancelled:
136146
fatalError("Unexpectedly got gracefulShutdown from group.next()")
137-
138147
case nil:
139148
fatalError("Unexpectedly got nil from group.next()")
140149
}

Tests/ServiceLifecycleTests/GracefulShutdownTests.swift

+84
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,88 @@ final class GracefulShutdownTests: XCTestCase {
353353
group.cancelAll()
354354
}
355355
}
356+
357+
func testCancelOnGracefulShutdownSurvivesCancellation() async throws {
358+
await withTaskGroup(of: Void.self) { group in
359+
group.addTask {
360+
await withGracefulShutdownHandler {
361+
await cancelOnGracefulShutdown {
362+
await OnlyCancellationWaiter().cancellation
363+
364+
try! await uncancellable {
365+
try! await Task.sleep(for: .milliseconds(500))
366+
}
367+
}
368+
} onGracefulShutdown: {
369+
XCTFail("Unexpect graceful shutdown")
370+
}
371+
}
372+
373+
group.cancelAll()
374+
}
375+
}
376+
377+
func testCancelOnGracefulShutdownSurvivesErrorThrown() async throws {
378+
struct MyError: Error, Equatable {}
379+
380+
await withTaskGroup(of: Void.self) { group in
381+
group.addTask {
382+
do {
383+
try await withGracefulShutdownHandler {
384+
try await cancelOnGracefulShutdown {
385+
await OnlyCancellationWaiter().cancellation
386+
387+
try! await uncancellable {
388+
try! await Task.sleep(for: .milliseconds(500))
389+
}
390+
391+
throw MyError()
392+
}
393+
} onGracefulShutdown: {
394+
XCTFail("Unexpect graceful shutdown")
395+
}
396+
XCTFail("Expected to have thrown")
397+
} catch {
398+
XCTAssertEqual(error as? MyError, MyError())
399+
}
400+
}
401+
402+
group.cancelAll()
403+
}
404+
}
405+
}
406+
407+
func uncancellable(_ closure: @escaping @Sendable () async throws -> Void) async throws {
408+
let task = Task {
409+
try await closure()
410+
}
411+
412+
try await task.value
413+
}
414+
415+
private actor OnlyCancellationWaiter {
416+
private var taskContinuation: CheckedContinuation<Void, Never>?
417+
418+
@usableFromInline
419+
init() {}
420+
421+
@usableFromInline
422+
var cancellation: Void {
423+
get async {
424+
await withTaskCancellationHandler {
425+
await withCheckedContinuation { continuation in
426+
self.taskContinuation = continuation
427+
}
428+
} onCancel: {
429+
Task {
430+
await self.finish()
431+
}
432+
}
433+
}
434+
}
435+
436+
private func finish() {
437+
self.taskContinuation?.resume()
438+
self.taskContinuation = nil
439+
}
356440
}

0 commit comments

Comments
 (0)