Skip to content

Commit 7e412d6

Browse files
committed
Code review and expose new cancelOnGracefulShutdown method
1 parent 7cab86b commit 7e412d6

File tree

5 files changed

+165
-72
lines changed

5 files changed

+165
-72
lines changed

Sources/ServiceLifecycle/GracefulShutdown.swift

Lines changed: 108 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
// See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors
1010
//
1111
// SPDX-License-Identifier: Apache-2.0
12-
//
12+
13+
import ConcurrencyHelpers
1314

1415
/// Execute an operation with a graceful shutdown handler that’s immediately invoked if the current task is shutting down gracefully.
1516
///
@@ -25,28 +26,83 @@
2526
/// trigger the quiescing sequence. Furthermore, graceful shutdown will propagate to any child task that is currently executing
2627
///
2728
/// - Parameters:
29+
/// - requiresRunningInsideServiceRunner: Indicates if this method requires to be run from within a ``ServiceRunner`` child task.
30+
/// This defaults to `true` and if run outside of a ``ServiceRunner`` child task will `fatalError`. If set to `false` then
31+
/// no graceful shutdown handler will be setup if not called inside a ``ServiceRunner`` child task. This is useful for code that
32+
/// can run both inside and outside of``ServiceRunner`` child tasks.
2833
/// - operation: The actual operation.
2934
/// - handler: The handler which is invoked once graceful shutdown has been triggered.
35+
// Unsafely inheriting the executor is safe to do here since we are not calling any other async method
36+
// except the operation. This makes sure no other executor hops would occur here.
37+
@_unsafeInheritExecutor
3038
public func withGracefulShutdownHandler<T>(
31-
@_inheritActorContext operation: @Sendable () async throws -> T,
39+
requiresRunningInsideServiceRunner: Bool = true,
40+
operation: () async throws -> T,
3241
onGracefulShutdown handler: @Sendable @escaping () -> Void
3342
) async rethrows -> T {
3443
guard let gracefulShutdownManager = TaskLocals.gracefulShutdownManager else {
35-
print("WARNING: Trying to setup a graceful shutdown handler inside a task that doesn't have access to the ShutdownGracefulManager. This happens either when unstructured Concurrency is used like Task.detached {} or when you tried to setup a shutdown graceful handler outside the ServiceRunner.run method. Not setting up the handler.")
36-
return try await operation()
44+
if !requiresRunningInsideServiceRunner {
45+
return try await operation()
46+
} else {
47+
fatalError("Trying to setup a graceful shutdown handler inside a task that doesn't have access to the ShutdownGracefulManager. This happens either when unstructured Concurrency is used like Task.detached {} or when you tried to setup a shutdown graceful handler outside the ServiceRunner.run method. Not setting up the handler.")
48+
}
3749
}
3850

3951
// We have to keep track of our handler here to remove it once the operation is finished.
40-
let handlerID = await gracefulShutdownManager.registerHandler(handler)
52+
let handlerID = gracefulShutdownManager.registerHandler(handler)
53+
defer {
54+
if let handlerID = handlerID {
55+
gracefulShutdownManager.removeHandler(handlerID)
56+
}
57+
}
4158

42-
let result = try await operation()
59+
return try await operation()
60+
}
4361

44-
// Great the operation is finished. If we have a number we need to remove the handler.
45-
if let handlerID {
46-
await gracefulShutdownManager.removeHandler(handlerID)
47-
}
62+
enum ValueOrGracefulShutdown<T> {
63+
case value(T)
64+
case gracefulShutdown
65+
}
66+
67+
/// Cancels the closure when a graceful shutdown was triggered.
68+
///
69+
/// - Parameter operation: The actual operation.
70+
public func cancelOnGracefulShutdown<T>(_ operation: @Sendable @escaping () async throws -> T) async rethrows -> T? {
71+
return try await withThrowingTaskGroup(of: ValueOrGracefulShutdown<T>.self) { group in
72+
group.addTask {
73+
let value = try await operation()
74+
return .value(value)
75+
}
76+
77+
group.addTask {
78+
for await _ in AsyncGracefulShutdownSequence() {
79+
return .gracefulShutdown
80+
}
81+
82+
throw CancellationError()
83+
}
4884

49-
return result
85+
let result = try await group.next()
86+
87+
switch result {
88+
case .value(let t):
89+
return t
90+
case .gracefulShutdown:
91+
group.cancelAll()
92+
switch try await group.next() {
93+
case .value(let t):
94+
return t
95+
case .gracefulShutdown:
96+
fatalError("Unexpectedly got gracefulShutdown from group.next()")
97+
98+
case nil:
99+
fatalError("Unexpectedly got nil from group.next()")
100+
}
101+
102+
case nil:
103+
fatalError("Unexpectedly got nil from group.next()")
104+
}
105+
}
50106
}
51107

52108
@_spi(TestKit)
@@ -57,59 +113,70 @@ public enum TaskLocals {
57113
}
58114

59115
@_spi(TestKit)
60-
public actor GracefulShutdownManager {
116+
public final class GracefulShutdownManager: @unchecked Sendable {
61117
struct Handler {
62118
/// The id of the handler.
63119
var id: UInt64
64120
/// The actual handler.
65121
var handler: () -> Void
66122
}
67123

68-
/// The currently registered handlers.
69-
private var handlers = [Handler]()
70-
/// A counter to assign a unique number to each handler.
71-
private var handlerCounter: UInt64 = 0
72-
/// A boolean indicating if we have been shutdown already.
73-
private var isShuttingDown = false
124+
struct State {
125+
/// The currently registered handlers.
126+
fileprivate var handlers = [Handler]()
127+
/// A counter to assign a unique number to each handler.
128+
fileprivate var handlerCounter: UInt64 = 0
129+
/// A boolean indicating if we have been shutdown already.
130+
fileprivate var isShuttingDown = false
131+
}
132+
133+
private let state = LockedValueBox(State())
74134

75135
@_spi(TestKit)
76136
public init() {}
77137

78138
func registerHandler(_ handler: @Sendable @escaping () -> Void) -> UInt64? {
79-
if self.isShuttingDown {
80-
handler()
81-
return nil
82-
} else {
83-
defer {
84-
self.handlerCounter += 1
139+
return self.state.withLockedValue { state in
140+
if state.isShuttingDown {
141+
// We are already shutting down so we just run the handler now.
142+
handler()
143+
return nil
144+
} else {
145+
defer {
146+
state.handlerCounter += 1
147+
}
148+
let handlerID = state.handlerCounter
149+
state.handlers.append(.init(id: handlerID, handler: handler))
150+
151+
return handlerID
85152
}
86-
let handlerID = self.handlerCounter
87-
self.handlers.append(.init(id: handlerID, handler: handler))
88-
89-
return handlerID
90153
}
91154
}
92155

93156
func removeHandler(_ handlerID: UInt64) {
94-
guard let index = self.handlers.firstIndex(where: { $0.id == handlerID }) else {
95-
// This can happen because if shutdownGracefully ran while the operation was still in progress
96-
return
97-
}
157+
self.state.withLockedValue { state in
158+
guard let index = state.handlers.firstIndex(where: { $0.id == handlerID }) else {
159+
// This can happen because if shutdownGracefully ran while the operation was still in progress
160+
return
161+
}
98162

99-
self.handlers.remove(at: index)
163+
state.handlers.remove(at: index)
164+
}
100165
}
101166

102167
@_spi(TestKit)
103168
public func shutdownGracefully() {
104-
guard !self.isShuttingDown else {
105-
return
106-
}
107-
self.isShuttingDown = true
169+
self.state.withLockedValue { state in
170+
guard !state.isShuttingDown else {
171+
return
172+
}
173+
state.isShuttingDown = true
108174

109-
for handler in self.handlers {
110-
handler.handler()
111-
}
175+
for handler in state.handlers {
176+
handler.handler()
177+
}
112178

113-
self.handlers.removeAll()
179+
state.handlers.removeAll()
180+
}
114181
}
115182
}

Sources/ServiceLifecycle/ServiceRunner.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ public actor ServiceRunner: Sendable {
138138
self.logger.debug(
139139
"Starting service lifecycle",
140140
metadata: [
141-
self.configuration.logging.signalsKey: "\(self.configuration.gracefulShutdownSignals)",
142-
self.configuration.logging.servicesKey: "\(self.services)",
141+
self.configuration.logging.keys.signalsKey: "\(self.configuration.gracefulShutdownSignals)",
142+
self.configuration.logging.keys.servicesKey: "\(self.services)",
143143
]
144144
)
145145

@@ -188,7 +188,7 @@ public actor ServiceRunner: Sendable {
188188
self.logger.debug(
189189
"Starting service",
190190
metadata: [
191-
self.configuration.logging.serviceKey: "\(service)",
191+
self.configuration.logging.keys.serviceKey: "\(service)",
192192
]
193193
)
194194

@@ -223,7 +223,7 @@ public actor ServiceRunner: Sendable {
223223
self.logger.error(
224224
"Service finished unexpectedly. Cancelling all other services now",
225225
metadata: [
226-
self.configuration.logging.serviceKey: "\(service)",
226+
self.configuration.logging.keys.serviceKey: "\(service)",
227227
]
228228
)
229229

@@ -235,8 +235,8 @@ public actor ServiceRunner: Sendable {
235235
self.logger.error(
236236
"Service threw error. Cancelling all other services now",
237237
metadata: [
238-
self.configuration.logging.serviceKey: "\(service)",
239-
self.configuration.logging.errorKey: "\(error)",
238+
self.configuration.logging.keys.serviceKey: "\(service)",
239+
self.configuration.logging.keys.errorKey: "\(error)",
240240
]
241241
)
242242
group.cancelAll()
@@ -248,7 +248,7 @@ public actor ServiceRunner: Sendable {
248248
self.logger.debug(
249249
"Signal caught. Shutting down services",
250250
metadata: [
251-
self.configuration.logging.signalKey: "\(unixSignal)",
251+
self.configuration.logging.keys.signalKey: "\(unixSignal)",
252252
]
253253
)
254254

@@ -309,11 +309,11 @@ public actor ServiceRunner: Sendable {
309309
self.logger.debug(
310310
"Triggering graceful shutdown for service",
311311
metadata: [
312-
self.configuration.logging.serviceKey: "\(self.services[gracefulShutdownIndex])",
312+
self.configuration.logging.keys.serviceKey: "\(self.services[gracefulShutdownIndex])",
313313
]
314314
)
315315

316-
await gracefulShutdownManager.shutdownGracefully()
316+
gracefulShutdownManager.shutdownGracefully()
317317

318318
let result = await group.next()
319319

@@ -325,7 +325,7 @@ public actor ServiceRunner: Sendable {
325325
self.logger.debug(
326326
"Service finished",
327327
metadata: [
328-
self.configuration.logging.serviceKey: "\(service)",
328+
self.configuration.logging.keys.serviceKey: "\(service)",
329329
]
330330
)
331331
continue
@@ -334,7 +334,7 @@ public actor ServiceRunner: Sendable {
334334
self.logger.debug(
335335
"Service finished unexpectedly during graceful shutdown. Cancelling all other services now",
336336
metadata: [
337-
self.configuration.logging.serviceKey: "\(service)",
337+
self.configuration.logging.keys.serviceKey: "\(service)",
338338
]
339339
)
340340

@@ -346,8 +346,8 @@ public actor ServiceRunner: Sendable {
346346
self.logger.debug(
347347
"Service threw error during graceful shutdown. Cancelling all other services now",
348348
metadata: [
349-
self.configuration.logging.serviceKey: "\(service)",
350-
self.configuration.logging.errorKey: "\(error)",
349+
self.configuration.logging.keys.serviceKey: "\(service)",
350+
self.configuration.logging.keys.errorKey: "\(error)",
351351
]
352352
)
353353
group.cancelAll()

Sources/ServiceLifecycle/ServiceRunnerConfiguration.swift

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,24 @@ import UnixSignals
1818
public struct ServiceRunnerConfiguration: Hashable, Sendable {
1919
/// The runner's logging configuration.
2020
public struct LoggingConfiguration: Hashable, Sendable {
21-
/// The logging key used for logging the unix signal.
22-
public var signalKey = "signal"
23-
/// The logging key used for logging the unix signals.
24-
public var signalsKey = "signals"
25-
/// The logging key used for logging the service.
26-
public var serviceKey = "service"
27-
/// The logging key used for logging the services.
28-
public var servicesKey = "services"
29-
/// The logging key used for logging an error.
30-
public var errorKey = "error"
21+
public struct Keys: Hashable, Sendable {
22+
/// The logging key used for logging the unix signal.
23+
public var signalKey = "signal"
24+
/// The logging key used for logging the unix signals.
25+
public var signalsKey = "signals"
26+
/// The logging key used for logging the service.
27+
public var serviceKey = "service"
28+
/// The logging key used for logging the services.
29+
public var servicesKey = "services"
30+
/// The logging key used for logging an error.
31+
public var errorKey = "error"
32+
33+
/// Initializes a new ``ServiceRunnerConfiguration/LoggingConfiguration/Keys``.
34+
public init() {}
35+
}
36+
37+
/// The keys used for logging.
38+
public var keys = Keys()
3139

3240
/// Initializes a new ``ServiceRunnerConfiguration/LoggingConfiguration``.
3341
public init() {}

Sources/ServiceLifecycleTestKit/GracefulShutdown.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public struct GracefulShutdownTestTrigger: Sendable {
2525
}
2626

2727
/// Triggers the graceful shutdown.
28-
public func triggerGracefulShutdown() async {
29-
await self.gracefulShutdownManager.shutdownGracefully()
28+
public func triggerGracefulShutdown() {
29+
self.gracefulShutdownManager.shutdownGracefully()
3030
}
3131
}
3232

0 commit comments

Comments
 (0)