Skip to content

Commit 943b33f

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 e15aacc commit 943b33f

File tree

7 files changed

+492
-270
lines changed

7 files changed

+492
-270
lines changed

Sources/ServiceLauncher/Lifecycle.swift

Lines changed: 183 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,8 @@ import Backtrace
2121
import Dispatch
2222
import Logging
2323

24-
/// `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()
28-
29-
private var state = State.idle
30-
private let stateSemaphore = DispatchSemaphore(value: 1)
31-
32-
private var items = [LifecycleItem]()
33-
private let itemsSemaphore = DispatchSemaphore(value: 1)
34-
35-
/// Creates a `Lifecycle` instance.
36-
public init() {
37-
self.shutdownGroup.enter()
38-
}
24+
public struct TopLevelLifecycle {
25+
private let lifecycle = Lifecycle(label: "\(TopLevelLifecycle.self)")
3926

4027
/// Starts the provided `LifecycleItem` array and waits (blocking) until a shutdown `Signal` is captured or `Lifecycle.shutdown` is called on another thread.
4128
/// Startup is performed in the order of items provided.
@@ -45,14 +32,13 @@ public class Lifecycle {
4532
public func startAndWait(configuration: Configuration = .init()) throws {
4633
let waitSemaphore = DispatchSemaphore(value: 0)
4734
var startError: Error?
48-
let items = self.itemsSemaphore.lock { self.items }
49-
self._start(configuration: configuration, items: items) { error in
35+
self.start(configuration: configuration) { error in
5036
startError = error
5137
waitSemaphore.signal()
5238
}
5339
waitSemaphore.wait()
5440
try startError.map { throw $0 }
55-
self.shutdownGroup.wait()
41+
self.wait()
5642
}
5743

5844
/// Starts the provided `LifecycleItem` array.
@@ -62,13 +48,151 @@ public class Lifecycle {
6248
/// - configuration: Defines lifecycle `Configuration`
6349
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
6450
public func start(configuration: Configuration = .init(), callback: @escaping (Error?) -> Void) {
51+
if configuration.installBacktrace {
52+
self.lifecycle.logger.info("installing backtrace")
53+
Backtrace.install()
54+
}
55+
configuration.shutdownSignal?.forEach { signal in
56+
self.lifecycle.logger.info("setting up shutdown hook on \(signal)")
57+
let signalSource = TopLevelLifecycle.trap(signal: signal, handler: { signal in
58+
self.lifecycle.logger.info("intercepted signal: \(signal)")
59+
self.shutdown(on: configuration.callbackQueue)
60+
})
61+
self.lifecycle.shutdownGroup.notify(queue: DispatchQueue.global()) {
62+
signalSource.cancel()
63+
}
64+
}
65+
self.lifecycle.start(on: configuration.callbackQueue) { error in
66+
callback(error)
67+
}
68+
}
69+
70+
/// Shuts down the `LifecycleItem` array provided in `start` or `startAndWait`.
71+
/// Shutdown is performed in reverse order of items provided./
72+
///
73+
/// - parameters:
74+
/// - on: `DispatchQueue` to run the handler on
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(on queue: DispatchQueue = DispatchQueue.global(), callback: @escaping (Error?) -> Void = { _ in }) {
77+
self.lifecycle.shutdown(on: queue, 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+
140+
/// `Lifecycle` provides a basic mechanism to cleanly startup and shutdown the application, freeing resources in order before exiting.
141+
public class Lifecycle: LifecycleItem {
142+
public let label: String
143+
internal let logger: Logger
144+
145+
private var state = State.idle
146+
private let stateSemaphore = DispatchSemaphore(value: 1)
147+
148+
private var items = [LifecycleItem]()
149+
private let itemsSemaphore = DispatchSemaphore(value: 1)
150+
151+
internal let shutdownGroup = DispatchGroup()
152+
153+
/// Creates a `Lifecycle` instance.
154+
public init(label: String) {
155+
self.label = label
156+
self.logger = Logger(label: "\(Lifecycle.self) \(label)")
157+
self.shutdownGroup.enter()
158+
}
159+
160+
/// Starts the provided `LifecycleItem` array.
161+
/// Startup is performed in the order of items provided.
162+
///
163+
/// - parameters:
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)
167+
}
168+
169+
/// Starts the provided `LifecycleItem` array.
170+
/// Startup is performed in the order of items provided.
171+
///
172+
/// - parameters:
173+
/// - on: `DispatchQueue` to run the handler on
174+
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
175+
public func start(on queue: DispatchQueue, callback: @escaping (Error?) -> Void) {
65176
let items = self.itemsSemaphore.lock { self.items }
66-
self._start(configuration: configuration, items: items, callback: callback)
177+
self._start(on: queue, items: items, callback: callback)
178+
}
179+
180+
/// Shuts down the `LifecycleItem` array provided in `start` or `startAndWait`.
181+
/// Shutdown is performed in reverse order of items provided.
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)
67187
}
68188

69189
/// Shuts down the `LifecycleItem` array provided in `start` or `startAndWait`.
70190
/// Shutdown is performed in reverse order of items provided.
71-
public func shutdown(on queue: DispatchQueue = DispatchQueue.global(), callback: @escaping ([String: Error]) -> Void = { _ in }) {
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 }) {
72196
self.stateSemaphore.wait()
73197
switch self.state {
74198
case .idle:
@@ -85,32 +209,28 @@ public class Lifecycle {
85209
self.stateSemaphore.signal()
86210
self._shutdown(on: queue, items: items) { errors in
87211
defer { self.shutdownGroup.leave() }
88-
callback(errors)
212+
callback(errors.count > 0 ? ShutdownError(errors: errors) : nil)
89213
}
90214
}
91215
}
92216

93-
/// Waits (blocking) until shutdown signal is captured or `Lifecycle.shutdown` is invoked on another thread.
94217
public func wait() {
95218
self.shutdownGroup.wait()
96219
}
97220

98221
// MARK: - private
99222

100-
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) {
101224
self.stateSemaphore.lock {
102225
guard case .idle = self.state else {
103226
preconditionFailure("invalid state, \(self.state)")
104227
}
105228
precondition(items.count > 0, "invalid number of items, must be > 0")
106229
self.logger.info("starting lifecycle")
107-
if configuration.installBacktrace {
108-
self.logger.info("installing backtrace")
109-
Backtrace.install()
110-
}
230+
111231
self.state = .starting
112232
}
113-
self._start(on: configuration.callbackQueue, items: items, index: 0) { _, error in
233+
self._start(on: queue, items: items, index: 0) { _, error in
114234
self.stateSemaphore.wait()
115235
if error != nil {
116236
self.state = .shuttingDown
@@ -119,23 +239,13 @@ public class Lifecycle {
119239
case .shuttingDown:
120240
self.stateSemaphore.signal()
121241
// shutdown was called while starting, or start failed, shutdown what we can
122-
self._shutdown(on: configuration.callbackQueue, items: items) { _ in
242+
self._shutdown(on: queue, items: items) { _ in
123243
callback(error)
124244
self.shutdownGroup.leave()
125245
}
126246
case .starting:
127247
self.state = .started(items)
128248
self.stateSemaphore.signal()
129-
configuration.shutdownSignal?.forEach { signal in
130-
self.logger.info("setting up shutdown hook on \(signal)")
131-
let signalSource = Lifecycle.trap(signal: signal, handler: { signal in
132-
self.logger.info("intercepted signal: \(signal)")
133-
self.shutdown(on: configuration.callbackQueue)
134-
})
135-
self.shutdownGroup.notify(queue: DispatchQueue.global()) {
136-
signalSource.cancel()
137-
}
138-
}
139249
return callback(nil)
140250
default:
141251
preconditionFailure("invalid state, \(self.state)")
@@ -154,7 +264,7 @@ public class Lifecycle {
154264
self.logger.info("starting item [\(items[index].label)]")
155265
start { error in
156266
if let error = error {
157-
self.logger.info("failed to start [\(items[index].label)]: \(error)")
267+
self.logger.error("failed to start [\(items[index].label)]: \(error)")
158268
return callback(index, error)
159269
}
160270
// shutdown called while starting
@@ -211,6 +321,10 @@ public class Lifecycle {
211321
case shuttingDown
212322
case shutdown
213323
}
324+
325+
public struct ShutdownError: Error {
326+
let errors: [String: Error]
327+
}
214328
}
215329

216330
extension Lifecycle {
@@ -220,31 +334,11 @@ extension Lifecycle {
220334
let shutdown: Handler
221335

222336
func start(callback: @escaping (Error?) -> Void) {
223-
self.start.run(callback)
337+
self.start.run(callback: callback)
224338
}
225339

226340
func shutdown(callback: @escaping (Error?) -> Void) {
227-
self.shutdown.run(callback)
228-
}
229-
}
230-
}
231-
232-
extension Lifecycle {
233-
/// `Lifecycle` configuration options.
234-
public struct Configuration {
235-
/// Defines the `DispatchQueue` on which startup and shutdown handlers are executed.
236-
public let callbackQueue: DispatchQueue
237-
/// Defines if to install a crash signal trap that prints backtraces.
238-
public let shutdownSignal: [Signal]?
239-
/// Defines what, if any, signals to trap for invoking shutdown.
240-
public let installBacktrace: Bool
241-
242-
public init(callbackQueue: DispatchQueue = DispatchQueue.global(),
243-
shutdownSignal: [Signal]? = [.TERM, .INT],
244-
installBacktrace: Bool = true) {
245-
self.callbackQueue = callbackQueue
246-
self.shutdownSignal = shutdownSignal
247-
self.installBacktrace = installBacktrace
341+
self.shutdown.run(callback: callback)
248342
}
249343
}
250344
}
@@ -322,7 +416,7 @@ public extension Lifecycle {
322416
///
323417
/// - parameters:
324418
/// - callback: the underlying completion handler
325-
public init(_ callback: @escaping (@escaping (Error?) -> Void) -> Void) {
419+
public init(callback: @escaping (@escaping (Error?) -> Void) -> Void) {
326420
self.body = callback
327421
}
328422

@@ -331,20 +425,20 @@ public extension Lifecycle {
331425
/// - parameters:
332426
/// - callback: the underlying completion handler
333427
public static func async(_ callback: @escaping (@escaping (Error?) -> Void) -> Void) -> Handler {
334-
return Handler(callback)
428+
return Handler(callback: callback)
335429
}
336430

337431
/// Asynchronous `Lifecycle.Handler` based on a blocking, throwing function.
338432
///
339433
/// - parameters:
340434
/// - body: the underlying function
341435
public static func sync(_ body: @escaping () throws -> Void) -> Handler {
342-
return Handler { completionHandler in
436+
return Handler { callback in
343437
do {
344438
try body()
345-
completionHandler(nil)
439+
callback(nil)
346440
} catch {
347-
completionHandler(error)
441+
callback(error)
348442
}
349443
}
350444
}
@@ -356,7 +450,7 @@ public extension Lifecycle {
356450
}
357451
}
358452

359-
internal func run(_ callback: @escaping (Error?) -> Void) {
453+
internal func run(callback: @escaping (Error?) -> Void) {
360454
self.body(callback)
361455
}
362456
}
@@ -369,7 +463,27 @@ public protocol LifecycleItem {
369463
func shutdown(callback: @escaping (Error?) -> Void)
370464
}
371465

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

Tests/LinuxMain.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import XCTest
2626
@testable import ServiceLauncherTests
2727

2828
XCTMain([
29-
testCase(Tests.allTests),
29+
testCase(LifeycleTests.allTests),
30+
testCase(TopLevelTests.allTests),
3031
])
3132
#endif

0 commit comments

Comments
 (0)