Skip to content

Commit e6b78a8

Browse files
authored
add support for stateful handler (#84)
motivation: allow lifecycle handlers to have the library manage the state for them so they do not need to do that manually changes: * introduce LifecycleStartHandler and LifecycleShutdownHandler which can handle state on behalf of the lifecycle item * add registerStateful function to regsiter stateful handlers * add tests
1 parent faa9f17 commit e6b78a8

File tree

5 files changed

+480
-14
lines changed

5 files changed

+480
-14
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,32 @@ In more complex cases, when `Signal`-trapping-based shutdown is not appropriate,
214214

215215
`shutdown` is an asynchronous operation. Errors will be logged and bubbled up to the provided completion handler.
216216

217+
### Stateful handlers
218+
219+
In some cases it is useful to have the Start handlers return a state that can be passed on to the Shutdown handlers for shutdown.
220+
For example, when establishing some sort of a connection that needs to be closed at shutdown.
221+
222+
```swift
223+
struct Foo {
224+
func start() throws -> Connection {
225+
return ...
226+
}
227+
228+
func shutdown(state: Connection) throws {
229+
...
230+
}
231+
}
232+
```
233+
234+
```swift
235+
let foo = ...
236+
lifecycle.registerStateful(
237+
label: "foo",
238+
start: .sync(foo.start),
239+
shutdown: .sync(foo.shutdown)
240+
)
241+
```
242+
217243
### Complex Systems and Nesting of Subsystems
218244

219245
In larger Applications (Services) `ComponentLifecycle` can be used to manage the lifecycle of subsystems, such that `ServiceLifecycle` can start and shutdown `ComponentLifecycle`s.

Sources/Lifecycle/Lifecycle.swift

Lines changed: 145 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,25 @@ extension LifecycleTask {
4242

4343
/// Supported startup and shutdown method styles
4444
public struct LifecycleHandler {
45+
@available(*, deprecated)
4546
public typealias Callback = (@escaping (Error?) -> Void) -> Void
4647

47-
private let body: Callback?
48+
private let underlying: ((@escaping (Error?) -> Void) -> Void)?
4849

4950
/// Initialize a `LifecycleHandler` based on a completion handler.
5051
///
5152
/// - parameters:
52-
/// - callback: the underlying completion handler
53-
/// - noop: the underlying completion handler is a no-op
54-
public init(_ callback: Callback?) {
55-
self.body = callback
53+
/// - handler: the underlying completion handler
54+
public init(_ handler: ((@escaping (Error?) -> Void) -> Void)?) {
55+
self.underlying = handler
5656
}
5757

5858
/// Asynchronous `LifecycleHandler` based on a completion handler.
5959
///
6060
/// - parameters:
61-
/// - callback: the underlying completion handler
62-
public static func async(_ callback: @escaping Callback) -> LifecycleHandler {
63-
return LifecycleHandler(callback)
61+
/// - handler: the underlying async handler
62+
public static func async(_ handler: @escaping (@escaping (Error?) -> Void) -> Void) -> LifecycleHandler {
63+
return LifecycleHandler(handler)
6464
}
6565

6666
/// Asynchronous `LifecycleHandler` based on a blocking, throwing function.
@@ -83,15 +83,97 @@ public struct LifecycleHandler {
8383
return LifecycleHandler(nil)
8484
}
8585

86-
internal func run(_ callback: @escaping (Error?) -> Void) {
87-
let body = self.body ?? { callback in
86+
internal func run(_ completionHandler: @escaping (Error?) -> Void) {
87+
let body = self.underlying ?? { callback in
8888
callback(nil)
8989
}
90-
body(callback)
90+
body(completionHandler)
9191
}
9292

9393
internal var noop: Bool {
94-
return self.body == nil
94+
return self.underlying == nil
95+
}
96+
}
97+
98+
// MARK: - Stateful Lifecycle Handlers
99+
100+
/// LifecycleHandler for starting stateful tasks. The state can then be fed into a LifecycleShutdownHandler
101+
public struct LifecycleStartHandler<State> {
102+
private let underlying: (@escaping (Result<State, Error>) -> Void) -> Void
103+
104+
/// Initialize a `LifecycleHandler` based on a completion handler.
105+
///
106+
/// - parameters:
107+
/// - callback: the underlying completion handler
108+
public init(_ handler: @escaping (@escaping (Result<State, Error>) -> Void) -> Void) {
109+
self.underlying = handler
110+
}
111+
112+
/// Asynchronous `LifecycleStartHandler` based on a completion handler.
113+
///
114+
/// - parameters:
115+
/// - handler: the underlying async handler
116+
public static func async(_ handler: @escaping (@escaping (Result<State, Error>) -> Void) -> Void) -> LifecycleStartHandler {
117+
return LifecycleStartHandler(handler)
118+
}
119+
120+
/// Synchronous `LifecycleStartHandler` based on a blocking, throwing function.
121+
///
122+
/// - parameters:
123+
/// - body: the underlying function
124+
public static func sync(_ body: @escaping () throws -> State) -> LifecycleStartHandler {
125+
return LifecycleStartHandler { completionHandler in
126+
do {
127+
let state = try body()
128+
completionHandler(.success(state))
129+
} catch {
130+
completionHandler(.failure(error))
131+
}
132+
}
133+
}
134+
135+
internal func run(_ completionHandler: @escaping (Result<State, Error>) -> Void) {
136+
self.underlying(completionHandler)
137+
}
138+
}
139+
140+
/// LifecycleHandler for shutting down stateful tasks. The state comes from a LifecycleStartHandler
141+
public struct LifecycleShutdownHandler<State> {
142+
private let underlying: (State, @escaping (Error?) -> Void) -> Void
143+
144+
/// Initialize a `LifecycleShutdownHandler` based on a completion handler.
145+
///
146+
/// - parameters:
147+
/// - handler: the underlying completion handler
148+
public init(_ handler: @escaping (State, @escaping (Error?) -> Void) -> Void) {
149+
self.underlying = handler
150+
}
151+
152+
/// Asynchronous `LifecycleShutdownHandler` based on a completion handler.
153+
///
154+
/// - parameters:
155+
/// - handler: the underlying async handler
156+
public static func async(_ handler: @escaping (State, @escaping (Error?) -> Void) -> Void) -> LifecycleShutdownHandler {
157+
return LifecycleShutdownHandler(handler)
158+
}
159+
160+
/// Asynchronous `LifecycleShutdownHandler` based on a blocking, throwing function.
161+
///
162+
/// - parameters:
163+
/// - body: the underlying function
164+
public static func sync(_ body: @escaping (State) throws -> Void) -> LifecycleShutdownHandler {
165+
return LifecycleShutdownHandler { state, completionHandler in
166+
do {
167+
try body(state)
168+
completionHandler(nil)
169+
} catch {
170+
completionHandler(error)
171+
}
172+
}
173+
}
174+
175+
internal func run(state: State, _ completionHandler: @escaping (Error?) -> Void) {
176+
self.underlying(state, completionHandler)
95177
}
96178
}
97179

@@ -550,8 +632,19 @@ extension LifecycleTasksContainer {
550632
public func registerShutdown(label: String, _ handler: LifecycleHandler) {
551633
self.register(label: label, start: .none, shutdown: handler)
552634
}
635+
636+
/// Add a stateful `LifecycleTask` to a `LifecycleTasks` collection.
637+
///
638+
/// - parameters:
639+
/// - label: label of the item, useful for debugging.
640+
/// - start: `LifecycleStartHandler` to perform the startup and return the state.
641+
/// - shutdown: `LifecycleShutdownHandler` to perform the shutdown given the state.
642+
public func registerStateful<State>(label: String, start: LifecycleStartHandler<State>, shutdown: LifecycleShutdownHandler<State>) {
643+
self.register(StatefulLifecycleTask(label: label, start: start, shutdown: shutdown))
644+
}
553645
}
554646

647+
// internal for testing
555648
internal struct _LifecycleTask: LifecycleTask {
556649
let label: String
557650
let shutdownIfNotStarted: Bool
@@ -573,3 +666,43 @@ internal struct _LifecycleTask: LifecycleTask {
573666
self.shutdown.run(callback)
574667
}
575668
}
669+
670+
// internal for testing
671+
internal class StatefulLifecycleTask<State>: LifecycleTask {
672+
let label: String
673+
let shutdownIfNotStarted: Bool = false
674+
let start: LifecycleStartHandler<State>
675+
let shutdown: LifecycleShutdownHandler<State>
676+
677+
let stateLock = Lock()
678+
var state: State?
679+
680+
init(label: String, start: LifecycleStartHandler<State>, shutdown: LifecycleShutdownHandler<State>) {
681+
self.label = label
682+
self.start = start
683+
self.shutdown = shutdown
684+
}
685+
686+
func start(_ callback: @escaping (Error?) -> Void) {
687+
self.start.run { result in
688+
switch result {
689+
case .failure(let error):
690+
callback(error)
691+
case .success(let state):
692+
self.stateLock.withLock {
693+
self.state = state
694+
}
695+
callback(nil)
696+
}
697+
}
698+
}
699+
700+
func shutdown(_ callback: @escaping (Error?) -> Void) {
701+
guard let state = (self.stateLock.withLock { self.state }) else {
702+
return callback(UnknownState())
703+
}
704+
self.shutdown.run(state: state, callback)
705+
}
706+
707+
struct UnknownState: Error {}
708+
}

Sources/LifecycleNIOCompat/Bridge.swift

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Lifecycle
1616
import NIO
1717

1818
extension LifecycleHandler {
19-
/// Asynchronous `Lifecycle.Handler` based on an `EventLoopFuture`.
19+
/// Asynchronous `LifecycleHandler` based on an `EventLoopFuture`.
2020
///
2121
/// - parameters:
2222
/// - future: function returning the underlying `EventLoopFuture`
@@ -32,8 +32,10 @@ extension LifecycleHandler {
3232
}
3333
}
3434
}
35+
}
3536

36-
/// `Lifecycle.Handler` that cancels a `RepeatedTask`.
37+
extension LifecycleHandler {
38+
/// `LifecycleHandler` that cancels a `RepeatedTask`.
3739
///
3840
/// - parameters:
3941
/// - task: `RepeatedTask` to be cancelled
@@ -47,6 +49,39 @@ extension LifecycleHandler {
4749
}
4850
}
4951

52+
extension LifecycleStartHandler {
53+
/// Asynchronous `LifecycleStartHandler` based on an `EventLoopFuture`.
54+
///
55+
/// - parameters:
56+
/// - future: function returning the underlying `EventLoopFuture`
57+
public static func eventLoopFuture(_ future: @escaping () -> EventLoopFuture<State>) -> LifecycleStartHandler {
58+
return LifecycleStartHandler { callback in
59+
future().whenComplete { result in
60+
callback(result)
61+
}
62+
}
63+
}
64+
}
65+
66+
extension LifecycleShutdownHandler {
67+
/// Asynchronous `LifecycleShutdownHandler` based on an `EventLoopFuture`.
68+
///
69+
/// - parameters:
70+
/// - future: function returning the underlying `EventLoopFuture`
71+
public static func eventLoopFuture(_ future: @escaping (State) -> EventLoopFuture<Void>) -> LifecycleShutdownHandler {
72+
return LifecycleShutdownHandler { state, callback in
73+
future(state).whenComplete { result in
74+
switch result {
75+
case .success:
76+
callback(nil)
77+
case .failure(let error):
78+
callback(error)
79+
}
80+
}
81+
}
82+
}
83+
}
84+
5085
extension ComponentLifecycle {
5186
/// Starts the provided `LifecycleItem` array.
5287
/// Startup is performed in the order of items provided.

Tests/LifecycleTests/ComponentLifecycleTests+XCTest.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ extension ComponentLifecycleTests {
5757
("testNOOPHandlers", testNOOPHandlers),
5858
("testShutdownOnlyStarted", testShutdownOnlyStarted),
5959
("testShutdownWhenStartFailedIfAsked", testShutdownWhenStartFailedIfAsked),
60+
("testStatefulSync", testStatefulSync),
61+
("testStatefulSyncStartError", testStatefulSyncStartError),
62+
("testStatefulSyncShutdownError", testStatefulSyncShutdownError),
63+
("testStatefulAsync", testStatefulAsync),
64+
("testStatefulAsyncStartError", testStatefulAsyncStartError),
65+
("testStatefulAsyncShutdownError", testStatefulAsyncShutdownError),
66+
("testStatefulNIO", testStatefulNIO),
67+
("testStatefulNIOStartFailure", testStatefulNIOStartFailure),
68+
("testStatefulNIOShutdownFailure", testStatefulNIOShutdownFailure),
6069
]
6170
}
6271
}

0 commit comments

Comments
 (0)