Skip to content

Commit ade5d0c

Browse files
committed
nested lifecycles
motivation: add ability to create a hirearchy of lifecycles to help manage more complex systems changes: * define top-level lifcycle where signal handling and backtraces are handled * conform Lifecycle to LifecycleItem so that lifecycles can be nested * change shutdown to return an error to better conform with LifecycleItem api
1 parent 85b55ce commit ade5d0c

File tree

7 files changed

+493
-277
lines changed

7 files changed

+493
-277
lines changed

Sources/ServiceLauncher/Lifecycle.swift

Lines changed: 184 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -21,61 +21,178 @@ import Backtrace
2121
import Dispatch
2222
import Logging
2323

24+
public struct TopLevelLifecycle {
25+
private let lifecycle = Lifecycle(label: "\(TopLevelLifecycle.self)")
26+
27+
/// Starts the provided `LifecycleItem` array and waits (blocking) until a shutdown `Signal` is captured or `Lifecycle.shutdown` is called on another thread.
28+
/// Startup is performed in the order of items provided.
29+
///
30+
/// - parameters:
31+
/// - configuration: Defines lifecycle `Configuration`
32+
public func startAndWait(configuration: Configuration = .init()) throws {
33+
let waitSemaphore = DispatchSemaphore(value: 0)
34+
var startError: Error?
35+
36+
self.start(configuration: configuration) { error in
37+
startError = error
38+
waitSemaphore.signal()
39+
}
40+
waitSemaphore.wait()
41+
try startError.map { throw $0 }
42+
self.wait()
43+
}
44+
45+
/// Starts the provided `LifecycleItem` array.
46+
/// Startup is performed in the order of items provided.
47+
///
48+
/// - parameters:
49+
/// - configuration: Defines lifecycle `Configuration`
50+
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
51+
public func start(configuration: Configuration = .init(), callback: @escaping (Error?) -> Void) {
52+
if configuration.installBacktrace {
53+
self.lifecycle.logger.info("installing backtrace")
54+
Backtrace.install()
55+
}
56+
configuration.shutdownSignal?.forEach { signal in
57+
self.lifecycle.logger.info("setting up shutdown hook on \(signal)")
58+
let signalSource = TopLevelLifecycle.trap(signal: signal, handler: { signal in
59+
self.lifecycle.logger.info("intercepted signal: \(signal)")
60+
self.shutdown()
61+
})
62+
self.lifecycle.shutdownGroup.notify(queue: DispatchQueue.global()) {
63+
signalSource.cancel()
64+
}
65+
}
66+
self.lifecycle.start(on: configuration.callbackQueue) { error in
67+
callback(error)
68+
}
69+
}
70+
71+
/// Shuts down the `LifecycleItem` array provided in `start` or `startAndWait`.
72+
/// Shutdown is performed in reverse order of items provided./
73+
///
74+
/// - parameters:
75+
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
76+
public func shutdown(callback: @escaping (Error?) -> Void = { _ in }) {
77+
self.lifecycle.shutdown(callback: callback)
78+
}
79+
80+
/// Waits (blocking) until shutdown signal is captured or `Lifecycle.shutdown` is invoked on another thread.
81+
public func wait() {
82+
self.lifecycle.wait()
83+
}
84+
85+
/// Adds a `LifecycleItem` to a `LifecycleItems` collection.
86+
///
87+
/// - parameters:
88+
/// - items: one or more `LifecycleItem`.
89+
func register(_ items: [LifecycleItem]) {
90+
self.lifecycle.register(items)
91+
}
92+
93+
/// Adds a `LifecycleItem` to a `LifecycleItems` collection.
94+
///
95+
/// - parameters:
96+
/// - items: one or more `LifecycleItem`.
97+
internal func register(_ items: LifecycleItem...) {
98+
self.lifecycle.register(items)
99+
}
100+
101+
/// Add a `LifecycleItem` to a `LifecycleItems` collection.
102+
///
103+
/// - parameters:
104+
/// - label: label of the item, useful for debugging.
105+
/// - start: closure to perform the startup.
106+
/// - shutdown: closure to perform the shutdown.
107+
func register(label: String, start: Lifecycle.Handler, shutdown: Lifecycle.Handler) {
108+
self.lifecycle.register(label: label, start: start, shutdown: shutdown)
109+
}
110+
111+
/// Adds a `LifecycleItem` to a `LifecycleItems` collection.
112+
///
113+
/// - parameters:
114+
/// - label: label of the item, useful for debugging.
115+
/// - shutdown: closure to perform the shutdown.
116+
func registerShutdown(label: String, _ handler: Lifecycle.Handler) {
117+
self.lifecycle.registerShutdown(label: label, handler)
118+
}
119+
120+
/// Adds a `LifecycleItem` to a `LifecycleItems` collection.
121+
///
122+
/// - parameters:
123+
/// - label: label of the item, useful for debugging.
124+
/// - start: closure to perform the shutdown.
125+
/// - shutdown: closure to perform the shutdown.
126+
func register(label: String, start: @escaping () throws -> Void, shutdown: @escaping () throws -> Void) {
127+
self.lifecycle.register(label: label, start: start, shutdown: shutdown)
128+
}
129+
130+
/// Adds a `LifecycleItem` to a `LifecycleItems` collection.
131+
///
132+
/// - parameters:
133+
/// - label: label of the item, useful for debugging.
134+
/// - shutdown: closure to perform the shutdown.
135+
func registerShutdown(label: String, _ handler: @escaping () throws -> Void) {
136+
self.lifecycle.registerShutdown(label: label, handler)
137+
}
138+
}
139+
24140
/// `Lifecycle` provides a basic mechanism to cleanly startup and shutdown the application, freeing resources in order before exiting.
25-
public class Lifecycle {
26-
private let logger = Logger(label: "\(Lifecycle.self)")
27-
private let shutdownGroup = DispatchGroup()
141+
public class Lifecycle: LifecycleItem {
142+
public let label: String
143+
internal let logger: Logger
28144

29145
private var state = State.idle
30146
private let stateLock = Lock()
31147

32148
private var items = [LifecycleItem]()
33149
private let itemsLock = Lock()
34150

151+
internal let shutdownGroup = DispatchGroup()
152+
35153
/// Creates a `Lifecycle` instance.
36-
public init() {
154+
public init(label: String) {
155+
self.label = label
156+
self.logger = Logger(label: "\(Lifecycle.self) \(label)")
37157
self.shutdownGroup.enter()
38158
}
39159

40-
/// Starts the provided `LifecycleItem` array and waits (blocking) until a shutdown `Signal` is captured or `Lifecycle.shutdown` is called on another thread.
160+
/// Starts the provided `LifecycleItem` array.
41161
/// Startup is performed in the order of items provided.
42162
///
43163
/// - parameters:
44-
/// - configuration: Defines lifecycle `Configuration`
45-
public func startAndWait(configuration: Configuration = .init()) throws {
46-
let waitLock = Lock()
47-
let group = DispatchGroup()
48-
var startError: Error?
49-
let items = self.itemsLock.withLock { self.items }
50-
group.enter()
51-
self._start(configuration: configuration, items: items) { error in
52-
waitLock.withLock {
53-
startError = error
54-
}
55-
group.leave()
56-
}
57-
self.shutdownGroup.wait()
58-
try waitLock.withLock {
59-
startError
60-
}.map { error in
61-
throw error
62-
}
164+
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
165+
public func start(callback: @escaping (Error?) -> Void) {
166+
self.start(on: DispatchQueue.global(), callback: callback)
63167
}
64168

65169
/// Starts the provided `LifecycleItem` array.
66170
/// Startup is performed in the order of items provided.
67171
///
68172
/// - parameters:
69-
/// - configuration: Defines lifecycle `Configuration`
173+
/// - on: `DispatchQueue` to run the handler on
70174
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
71-
public func start(configuration: Configuration = .init(), callback: @escaping (Error?) -> Void) {
175+
public func start(on queue: DispatchQueue, callback: @escaping (Error?) -> Void) {
72176
let items = self.itemsLock.withLock { self.items }
73-
self._start(configuration: configuration, items: items, callback: callback)
177+
self._start(on: queue, items: items, callback: callback)
74178
}
75179

76180
/// Shuts down the `LifecycleItem` array provided in `start` or `startAndWait`.
77181
/// Shutdown is performed in reverse order of items provided.
78-
public func shutdown(on queue: DispatchQueue = DispatchQueue.global(), callback: @escaping ([String: Error]) -> Void = { _ in }) {
182+
///
183+
/// - parameters:
184+
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
185+
public func shutdown(callback: @escaping (Error?) -> Void = { _ in }) {
186+
self.shutdown(on: DispatchQueue.global(), callback: callback)
187+
}
188+
189+
/// Shuts down the `LifecycleItem` array provided in `start` or `startAndWait`.
190+
/// Shutdown is performed in reverse order of items provided.
191+
///
192+
/// - parameters:
193+
/// - on: `DispatchQueue` to run the handler on
194+
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
195+
public func shutdown(on queue: DispatchQueue, callback: @escaping (Error?) -> Void = { _ in }) {
79196
self.stateLock.lock()
80197
switch self.state {
81198
case .idle:
@@ -92,32 +209,28 @@ public class Lifecycle {
92209
self.stateLock.unlock()
93210
self._shutdown(on: queue, items: items) { errors in
94211
defer { self.shutdownGroup.leave() }
95-
callback(errors)
212+
callback(errors.count > 0 ? ShutdownError(errors: errors) : nil)
96213
}
97214
}
98215
}
99216

100-
/// Waits (blocking) until shutdown signal is captured or `Lifecycle.shutdown` is invoked on another thread.
101217
public func wait() {
102218
self.shutdownGroup.wait()
103219
}
104220

105221
// MARK: - private
106222

107-
private func _start(configuration: Configuration, items: [LifecycleItem], callback: @escaping (Error?) -> Void) {
223+
private func _start(on queue: DispatchQueue = DispatchQueue.global(), items: [LifecycleItem], callback: @escaping (Error?) -> Void) {
108224
self.stateLock.withLock {
109225
guard case .idle = self.state else {
110226
preconditionFailure("invalid state, \(self.state)")
111227
}
112228
precondition(items.count > 0, "invalid number of items, must be > 0")
113229
self.logger.info("starting lifecycle")
114-
if configuration.installBacktrace {
115-
self.logger.info("installing backtrace")
116-
Backtrace.install()
117-
}
230+
118231
self.state = .starting
119232
}
120-
self._start(on: configuration.callbackQueue, items: items, index: 0) { _, error in
233+
self._start(on: queue, items: items, index: 0) { _, error in
121234
self.stateLock.lock()
122235
if error != nil {
123236
self.state = .shuttingDown
@@ -126,23 +239,13 @@ public class Lifecycle {
126239
case .shuttingDown:
127240
self.stateLock.unlock()
128241
// shutdown was called while starting, or start failed, shutdown what we can
129-
self._shutdown(on: configuration.callbackQueue, items: items) { _ in
242+
self._shutdown(on: queue, items: items) { _ in
130243
callback(error)
131244
self.shutdownGroup.leave()
132245
}
133246
case .starting:
134247
self.state = .started(items)
135248
self.stateLock.unlock()
136-
configuration.shutdownSignal?.forEach { signal in
137-
self.logger.info("setting up shutdown hook on \(signal)")
138-
let signalSource = Lifecycle.trap(signal: signal, handler: { signal in
139-
self.logger.info("intercepted signal: \(signal)")
140-
self.shutdown(on: configuration.callbackQueue)
141-
})
142-
self.shutdownGroup.notify(queue: DispatchQueue.global()) {
143-
signalSource.cancel()
144-
}
145-
}
146249
return callback(nil)
147250
default:
148251
preconditionFailure("invalid state, \(self.state)")
@@ -161,7 +264,7 @@ public class Lifecycle {
161264
self.logger.info("starting item [\(items[index].label)]")
162265
start { error in
163266
if let error = error {
164-
self.logger.info("failed to start [\(items[index].label)]: \(error)")
267+
self.logger.error("failed to start [\(items[index].label)]: \(error)")
165268
return callback(index, error)
166269
}
167270
// shutdown called while starting
@@ -215,6 +318,10 @@ public class Lifecycle {
215318
case shuttingDown
216319
case shutdown
217320
}
321+
322+
public struct ShutdownError: Error {
323+
let errors: [String: Error]
324+
}
218325
}
219326

220327
extension Lifecycle {
@@ -224,31 +331,11 @@ extension Lifecycle {
224331
let shutdown: Handler
225332

226333
func start(callback: @escaping (Error?) -> Void) {
227-
self.start.run(callback)
334+
self.start.run(callback: callback)
228335
}
229336

230337
func shutdown(callback: @escaping (Error?) -> Void) {
231-
self.shutdown.run(callback)
232-
}
233-
}
234-
}
235-
236-
extension Lifecycle {
237-
/// `Lifecycle` configuration options.
238-
public struct Configuration {
239-
/// Defines the `DispatchQueue` on which startup and shutdown handlers are executed.
240-
public let callbackQueue: DispatchQueue
241-
/// Defines if to install a crash signal trap that prints backtraces.
242-
public let shutdownSignal: [Signal]?
243-
/// Defines what, if any, signals to trap for invoking shutdown.
244-
public let installBacktrace: Bool
245-
246-
public init(callbackQueue: DispatchQueue = DispatchQueue.global(),
247-
shutdownSignal: [Signal]? = [.TERM, .INT],
248-
installBacktrace: Bool = true) {
249-
self.callbackQueue = callbackQueue
250-
self.shutdownSignal = shutdownSignal
251-
self.installBacktrace = installBacktrace
338+
self.shutdown.run(callback: callback)
252339
}
253340
}
254341
}
@@ -326,7 +413,7 @@ public extension Lifecycle {
326413
///
327414
/// - parameters:
328415
/// - callback: the underlying completion handler
329-
public init(_ callback: @escaping (@escaping (Error?) -> Void) -> Void) {
416+
public init(callback: @escaping (@escaping (Error?) -> Void) -> Void) {
330417
self.body = callback
331418
}
332419

@@ -335,20 +422,20 @@ public extension Lifecycle {
335422
/// - parameters:
336423
/// - callback: the underlying completion handler
337424
public static func async(_ callback: @escaping (@escaping (Error?) -> Void) -> Void) -> Handler {
338-
return Handler(callback)
425+
return Handler(callback: callback)
339426
}
340427

341428
/// Asynchronous `Lifecycle.Handler` based on a blocking, throwing function.
342429
///
343430
/// - parameters:
344431
/// - body: the underlying function
345432
public static func sync(_ body: @escaping () throws -> Void) -> Handler {
346-
return Handler { completionHandler in
433+
return Handler { callback in
347434
do {
348435
try body()
349-
completionHandler(nil)
436+
callback(nil)
350437
} catch {
351-
completionHandler(error)
438+
callback(error)
352439
}
353440
}
354441
}
@@ -360,7 +447,7 @@ public extension Lifecycle {
360447
}
361448
}
362449

363-
internal func run(_ callback: @escaping (Error?) -> Void) {
450+
internal func run(callback: @escaping (Error?) -> Void) {
364451
self.body(callback)
365452
}
366453
}
@@ -373,7 +460,27 @@ public protocol LifecycleItem {
373460
func shutdown(callback: @escaping (Error?) -> Void)
374461
}
375462

376-
extension Lifecycle {
463+
extension TopLevelLifecycle {
464+
/// `Lifecycle` configuration options.
465+
public struct Configuration {
466+
/// Defines the `DispatchQueue` on which startup and shutdown handlers are executed.
467+
public let callbackQueue: DispatchQueue
468+
/// Defines if to install a crash signal trap that prints backtraces.
469+
public let shutdownSignal: [Signal]?
470+
/// Defines what, if any, signals to trap for invoking shutdown.
471+
public let installBacktrace: Bool
472+
473+
public init(callbackQueue: DispatchQueue = DispatchQueue.global(),
474+
shutdownSignal: [Signal]? = [.TERM, .INT],
475+
installBacktrace: Bool = true) {
476+
self.callbackQueue = callbackQueue
477+
self.shutdownSignal = shutdownSignal
478+
self.installBacktrace = installBacktrace
479+
}
480+
}
481+
}
482+
483+
extension TopLevelLifecycle {
377484
/// Setup a signal trap.
378485
///
379486
/// - parameters:

0 commit comments

Comments
 (0)