Skip to content

Commit 45a31b6

Browse files
authored
Add async gracefulShutdown (#158)
1 parent b69c630 commit 45a31b6

File tree

5 files changed

+129
-19
lines changed

5 files changed

+129
-19
lines changed

Sources/ServiceLifecycle/AsyncGracefulShutdownSequence.swift

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
/// An async sequence that emits an element once graceful shutdown has been triggered.
1616
///
1717
/// This sequence is a broadcast async sequence and will only produce one value and then finish.
18+
///
19+
/// - Note: This sequence respects cancellation and thus is `throwing`.
1820
@usableFromInline
1921
struct AsyncGracefulShutdownSequence: AsyncSequence, Sendable {
2022
@usableFromInline
@@ -34,19 +36,9 @@ struct AsyncGracefulShutdownSequence: AsyncSequence, Sendable {
3436
init() {}
3537

3638
@inlinable
37-
func next() async -> Element? {
38-
var cont: AsyncStream<Void>.Continuation!
39-
let stream = AsyncStream<Void> { cont = $0 }
40-
let continuation = cont!
41-
42-
return await withTaskGroup(of: Void.self) { _ in
43-
await withGracefulShutdownHandler {
44-
await stream.first { _ in true }
45-
} onGracefulShutdown: {
46-
continuation.yield(())
47-
continuation.finish()
48-
}
49-
}
39+
func next() async throws -> Element? {
40+
try await CancellationWaiter().wait()
41+
return ()
5042
}
5143
}
5244
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftServiceLifecycle open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// An actor that provides a function to wait on cancellation/graceful shutdown.
16+
@usableFromInline
17+
actor CancellationWaiter {
18+
private var taskContinuation: CheckedContinuation<Void, Error>?
19+
20+
@usableFromInline
21+
init() {}
22+
23+
@usableFromInline
24+
func wait() async throws {
25+
try await withTaskCancellationHandler {
26+
try await withGracefulShutdownHandler {
27+
try await withCheckedThrowingContinuation { continuation in
28+
self.taskContinuation = continuation
29+
}
30+
} onGracefulShutdown: {
31+
Task {
32+
await self.finish()
33+
}
34+
}
35+
} onCancel: {
36+
Task {
37+
await self.finish(throwing: CancellationError())
38+
}
39+
}
40+
}
41+
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+
self.taskContinuation = nil
49+
}
50+
}

Sources/ServiceLifecycle/GracefulShutdown.swift

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

58+
/// Waits until graceful shutdown is triggered.
59+
///
60+
/// This method suspends the caller until graceful shutdown is triggered. If the calling task is cancelled before
61+
/// graceful shutdown is triggered then this method will throw a `CancellationError`.
62+
///
63+
/// - Throws: `CancellationError` if the task is cancelled.
64+
public func gracefulShutdown() async throws {
65+
try await AsyncGracefulShutdownSequence().first { _ in true }
66+
}
67+
5868
/// This is just a helper type for the result of our task group.
5969
enum ValueOrGracefulShutdown<T: Sendable>: Sendable {
6070
case value(T)
@@ -72,7 +82,7 @@ public func cancelOnGracefulShutdown<T: Sendable>(_ operation: @Sendable @escapi
7282
}
7383

7484
group.addTask {
75-
for await _ in AsyncGracefulShutdownSequence() {
85+
for try await _ in AsyncGracefulShutdownSequence() {
7686
return .gracefulShutdown
7787
}
7888

@@ -138,6 +148,8 @@ public final class GracefulShutdownManager: @unchecked Sendable {
138148
fileprivate var handlerCounter: UInt64 = 0
139149
/// A boolean indicating if we have been shutdown already.
140150
fileprivate var isShuttingDown = false
151+
/// Continuations to resume after all of the handlers have been executed.
152+
fileprivate var gracefulShutdownFinishedContinuations = [CheckedContinuation<Void, Never>]()
141153
}
142154

143155
private let state = LockedValueBox(State())
@@ -191,6 +203,12 @@ public final class GracefulShutdownManager: @unchecked Sendable {
191203
}
192204

193205
state.handlers.removeAll()
206+
207+
for continuation in state.gracefulShutdownFinishedContinuations {
208+
continuation.resume()
209+
}
210+
211+
state.gracefulShutdownFinishedContinuations.removeAll()
194212
}
195213
}
196214
}

Sources/ServiceLifecycle/ServiceGroup.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public actor ServiceGroup: Sendable {
193193

194194
// Using a result here since we want a task group that has non-throwing child tasks
195195
// but the body itself is throwing
196-
let result = await withTaskGroup(of: ChildTaskResult.self, returning: Result<Void, Error>.self) { group in
196+
let result = try await withThrowingTaskGroup(of: ChildTaskResult.self, returning: Result<Void, Error>.self) { group in
197197
// First we have to register our signals.
198198
let gracefulShutdownSignals = await UnixSignalsSequence(trapping: self.gracefulShutdownSignals)
199199
let cancellationSignals = await UnixSignalsSequence(trapping: self.cancellationSignals)
@@ -228,7 +228,7 @@ public actor ServiceGroup: Sendable {
228228
// This is an optional task that listens to graceful shutdowns from the parent task
229229
if let _ = TaskLocals.gracefulShutdownManager {
230230
group.addTask {
231-
for await _ in AsyncGracefulShutdownSequence() {
231+
for try await _ in AsyncGracefulShutdownSequence() {
232232
return .gracefulShutdownCaught
233233
}
234234

@@ -276,7 +276,7 @@ public actor ServiceGroup: Sendable {
276276
// We are going to wait for any of the services to finish or
277277
// the signal sequence to throw an error.
278278
while !group.isEmpty {
279-
let result: ChildTaskResult? = await group.next()
279+
let result: ChildTaskResult? = try await group.next()
280280

281281
switch result {
282282
case .serviceFinished(let service, let index):
@@ -452,7 +452,7 @@ public actor ServiceGroup: Sendable {
452452

453453
private func shutdownGracefully(
454454
services: [ServiceGroupConfiguration.ServiceConfiguration?],
455-
group: inout TaskGroup<ChildTaskResult>,
455+
group: inout ThrowingTaskGroup<ChildTaskResult, Error>,
456456
gracefulShutdownManagers: [GracefulShutdownManager]
457457
) async throws {
458458
guard case .running = self.state else {
@@ -481,7 +481,7 @@ public actor ServiceGroup: Sendable {
481481

482482
gracefulShutdownManager.shutdownGracefully()
483483

484-
let result = await group.next()
484+
let result = try await group.next()
485485

486486
switch result {
487487
case .serviceFinished(let service, let index):

Tests/ServiceLifecycleTests/GracefulShutdownTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,54 @@ final class GracefulShutdownTests: XCTestCase {
253253
XCTAssertTrue(Task.isShuttingDownGracefully)
254254
}
255255
}
256+
257+
func testWaitForGracefulShutdown() async throws {
258+
try await testGracefulShutdown { gracefulShutdownTestTrigger in
259+
try await withThrowingTaskGroup(of: Void.self) { group in
260+
group.addTask {
261+
try await Task.sleep(for: .milliseconds(10))
262+
gracefulShutdownTestTrigger.triggerGracefulShutdown()
263+
}
264+
265+
try await withGracefulShutdownHandler {
266+
try await gracefulShutdown()
267+
} onGracefulShutdown: {
268+
// No-op
269+
}
270+
271+
try await group.waitForAll()
272+
}
273+
}
274+
}
275+
276+
func testWaitForGracefulShutdown_WhenAlreadyShutdown() async throws {
277+
try await testGracefulShutdown { gracefulShutdownTestTrigger in
278+
gracefulShutdownTestTrigger.triggerGracefulShutdown()
279+
280+
try await withGracefulShutdownHandler {
281+
try await Task.sleep(for: .milliseconds(10))
282+
try await gracefulShutdown()
283+
} onGracefulShutdown: {
284+
// No-op
285+
}
286+
}
287+
}
288+
289+
func testWaitForGracefulShutdown_Cancellation() async throws {
290+
do {
291+
try await testGracefulShutdown { _ in
292+
try await withThrowingTaskGroup(of: Void.self) { group in
293+
group.addTask {
294+
try await gracefulShutdown()
295+
}
296+
297+
group.cancelAll()
298+
try await group.waitForAll()
299+
}
300+
}
301+
XCTFail("Expected CancellationError to be thrown")
302+
} catch {
303+
XCTAssertTrue(error is CancellationError)
304+
}
305+
}
256306
}

0 commit comments

Comments
 (0)