@@ -21,61 +21,178 @@ import Backtrace
21
21
import Dispatch
22
22
import Logging
23
23
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
+
24
140
/// `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
28
144
29
145
private var state = State . idle
30
146
private let stateLock = Lock ( )
31
147
32
148
private var items = [ LifecycleItem] ( )
33
149
private let itemsLock = Lock ( )
34
150
151
+ internal let shutdownGroup = DispatchGroup ( )
152
+
35
153
/// Creates a `Lifecycle` instance.
36
- public init ( ) {
154
+ public init ( label: String ) {
155
+ self . label = label
156
+ self . logger = Logger ( label: " \( Lifecycle . self) \( label) " )
37
157
self . shutdownGroup. enter ( )
38
158
}
39
159
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.
41
161
/// Startup is performed in the order of items provided.
42
162
///
43
163
/// - 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)
63
167
}
64
168
65
169
/// Starts the provided `LifecycleItem` array.
66
170
/// Startup is performed in the order of items provided.
67
171
///
68
172
/// - parameters:
69
- /// - configuration: Defines lifecycle `Configuration`
173
+ /// - on: `DispatchQueue` to run the handler on
70
174
/// - 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 ) {
72
176
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)
74
178
}
75
179
76
180
/// Shuts down the `LifecycleItem` array provided in `start` or `startAndWait`.
77
181
/// 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 } ) {
79
196
self . stateLock. lock ( )
80
197
switch self . state {
81
198
case . idle:
@@ -92,32 +209,28 @@ public class Lifecycle {
92
209
self . stateLock. unlock ( )
93
210
self . _shutdown ( on: queue, items: items) { errors in
94
211
defer { self . shutdownGroup. leave ( ) }
95
- callback ( errors)
212
+ callback ( errors. count > 0 ? ShutdownError ( errors : errors ) : nil )
96
213
}
97
214
}
98
215
}
99
216
100
- /// Waits (blocking) until shutdown signal is captured or `Lifecycle.shutdown` is invoked on another thread.
101
217
public func wait( ) {
102
218
self . shutdownGroup. wait ( )
103
219
}
104
220
105
221
// MARK: - private
106
222
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 ) {
108
224
self . stateLock. withLock {
109
225
guard case . idle = self . state else {
110
226
preconditionFailure ( " invalid state, \( self . state) " )
111
227
}
112
228
precondition ( items. count > 0 , " invalid number of items, must be > 0 " )
113
229
self . logger. info ( " starting lifecycle " )
114
- if configuration. installBacktrace {
115
- self . logger. info ( " installing backtrace " )
116
- Backtrace . install ( )
117
- }
230
+
118
231
self . state = . starting
119
232
}
120
- self . _start ( on: configuration . callbackQueue , items: items, index: 0 ) { _, error in
233
+ self . _start ( on: queue , items: items, index: 0 ) { _, error in
121
234
self . stateLock. lock ( )
122
235
if error != nil {
123
236
self . state = . shuttingDown
@@ -126,23 +239,13 @@ public class Lifecycle {
126
239
case . shuttingDown:
127
240
self . stateLock. unlock ( )
128
241
// 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
130
243
callback ( error)
131
244
self . shutdownGroup. leave ( )
132
245
}
133
246
case . starting:
134
247
self . state = . started( items)
135
248
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
- }
146
249
return callback ( nil )
147
250
default :
148
251
preconditionFailure ( " invalid state, \( self . state) " )
@@ -161,7 +264,7 @@ public class Lifecycle {
161
264
self . logger. info ( " starting item [ \( items [ index] . label) ] " )
162
265
start { error in
163
266
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) " )
165
268
return callback ( index, error)
166
269
}
167
270
// shutdown called while starting
@@ -215,6 +318,10 @@ public class Lifecycle {
215
318
case shuttingDown
216
319
case shutdown
217
320
}
321
+
322
+ public struct ShutdownError : Error {
323
+ let errors : [ String : Error ]
324
+ }
218
325
}
219
326
220
327
extension Lifecycle {
@@ -224,31 +331,11 @@ extension Lifecycle {
224
331
let shutdown : Handler
225
332
226
333
func start( callback: @escaping ( Error ? ) -> Void ) {
227
- self . start. run ( callback)
334
+ self . start. run ( callback: callback )
228
335
}
229
336
230
337
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)
252
339
}
253
340
}
254
341
}
@@ -326,7 +413,7 @@ public extension Lifecycle {
326
413
///
327
414
/// - parameters:
328
415
/// - callback: the underlying completion handler
329
- public init ( _ callback: @escaping ( @escaping ( Error ? ) -> Void ) -> Void ) {
416
+ public init ( callback: @escaping ( @escaping ( Error ? ) -> Void ) -> Void ) {
330
417
self . body = callback
331
418
}
332
419
@@ -335,20 +422,20 @@ public extension Lifecycle {
335
422
/// - parameters:
336
423
/// - callback: the underlying completion handler
337
424
public static func async ( _ callback: @escaping ( @escaping ( Error ? ) -> Void ) -> Void ) -> Handler {
338
- return Handler ( callback)
425
+ return Handler ( callback: callback )
339
426
}
340
427
341
428
/// Asynchronous `Lifecycle.Handler` based on a blocking, throwing function.
342
429
///
343
430
/// - parameters:
344
431
/// - body: the underlying function
345
432
public static func sync( _ body: @escaping ( ) throws -> Void ) -> Handler {
346
- return Handler { completionHandler in
433
+ return Handler { callback in
347
434
do {
348
435
try body ( )
349
- completionHandler ( nil )
436
+ callback ( nil )
350
437
} catch {
351
- completionHandler ( error)
438
+ callback ( error)
352
439
}
353
440
}
354
441
}
@@ -360,7 +447,7 @@ public extension Lifecycle {
360
447
}
361
448
}
362
449
363
- internal func run( _ callback: @escaping ( Error ? ) -> Void ) {
450
+ internal func run( callback: @escaping ( Error ? ) -> Void ) {
364
451
self . body ( callback)
365
452
}
366
453
}
@@ -373,7 +460,27 @@ public protocol LifecycleItem {
373
460
func shutdown( callback: @escaping ( Error ? ) -> Void )
374
461
}
375
462
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 {
377
484
/// Setup a signal trap.
378
485
///
379
486
/// - parameters:
0 commit comments