|
1 | 1 | # SwiftServiceLauncher
|
2 | 2 |
|
3 | 3 | SwiftServiceLauncher provides a basic mechanism to cleanly start up and shut down the application, freeing resources in order before exiting.
|
4 |
| -It also provides a Signal based shutdown hook, to shutdown on signals like TERM or INT. |
| 4 | +It also provides a `Signal`-based shutdown hook, to shutdown on signals like `TERM` or `INT`. |
5 | 5 |
|
6 |
| -SwiftServiceLauncher is non-framework specific, designed to be integrated with any server framework or directly in an application. |
| 6 | +SwiftServiceLauncher was designed with the idea that every application has some startup and shutdown workflow-like-logic which is often sensitive to failure and hard to get right. |
| 7 | +The library codes this common need in a safe and reusable way that is non-framework specific, and designed to be integrated with any server framework or directly in an application. |
7 | 8 |
|
8 |
| -## Usage |
| 9 | +This is the beginning of a community-driven open-source project actively seeking contributions, be it code, documentation, or ideas. What SwiftServiceLauncher provides today is covered in the [API docs](https://swift-server.github.io/swift-service-launcher/), but it will continue to evolve with community input. |
| 10 | + |
| 11 | +## Getting started |
| 12 | + |
| 13 | +If you have a server-side Swift application or a cross-platform (e.g. Linux, macOS) application, and you would like to manage its startup and shutdown lifecycle, SwiftServiceLauncher is a great idea. Below you will find all you need to know to get started. |
| 14 | + |
| 15 | +### Adding the dependency |
| 16 | + |
| 17 | +To add a dependency on the package, declare it in your `Package.swift`: |
| 18 | + |
| 19 | +```swift |
| 20 | +.package(url: "https://github.com/swift-server/swift-service-launcher.git", from: "1.0.0"), |
| 21 | +``` |
| 22 | + |
| 23 | +and to your application target, add "SwiftServiceLauncher" to your dependencies: |
| 24 | + |
| 25 | +```swift |
| 26 | +.target(name: "BestExampleApp", dependencies: ["SwiftServiceLauncher"]), |
| 27 | +``` |
| 28 | + |
| 29 | +### Defining the lifecycle |
9 | 30 |
|
10 | 31 | ```swift
|
| 32 | +// import the package |
| 33 | +import ServiceLauncher |
| 34 | + |
| 35 | +// initialize the lifecycle container |
11 | 36 | var lifecycle = Lifecycle()
|
12 | 37 |
|
| 38 | +// register a resource that should be shutdown when the application exists. |
| 39 | +// in this case, we are registering a SwiftNIO EventLoopGroup |
| 40 | +// and passing its `syncShutdownGracefully` function to be called on shutdown |
13 | 41 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
|
14 | 42 | lifecycle.registerShutdown(
|
15 | 43 | name: "eventLoopGroup",
|
16 | 44 | eventLoopGroup.syncShutdownGracefully
|
17 | 45 | )
|
18 | 46 |
|
| 47 | +// register another resource that should be shutdown when the application exits. |
| 48 | +// in this case, we are registering an HTTPClient |
| 49 | +// and passing its `syncShutdown` function to be called on shutdown |
19 | 50 | let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup))
|
20 | 51 | lifecycle.registerShutdown(
|
21 | 52 | name: "HTTPClient",
|
22 | 53 | httpClient.syncShutdown
|
23 | 54 | )
|
24 | 55 |
|
| 56 | +// start the application |
| 57 | +// start handlers passed using the `register` function |
| 58 | +// will be called in the order the items were registered in |
25 | 59 | lifecycle.start() { error in
|
| 60 | + // this is the start completion handler. |
| 61 | + // if an error occurred you can log it here |
26 | 62 | if let error = error {
|
27 | 63 | logger.error("failed starting \(self) ☠️: \(error)")
|
28 | 64 | } else {
|
29 | 65 | logger.info("\(self) started successfully 🚀")
|
30 | 66 | }
|
31 | 67 | }
|
| 68 | +// wait for the application to exist |
| 69 | +// this is a blocking operation that typically waits for |
| 70 | +// for a signal configured at lifecycle.start (default is `INT` and `TERM`) |
| 71 | +// or another thread calling lifecycle.shutdown (atypical) |
| 72 | +// shutdown handlers passed using the `register` or `registerShutdown` functions |
| 73 | +// will be called in the reverse order the items were registered in |
32 | 74 | lifecycle.wait()
|
33 | 75 | ```
|
| 76 | + |
| 77 | +## Detailed design |
| 78 | + |
| 79 | +The main type in the library is `Lifecycle` which manages a state machine representing the application's startup and shutdown logic. |
| 80 | + |
| 81 | +### Registering items |
| 82 | + |
| 83 | +`Lifecycle` is a container for `LifecycleItem`s which need to be registered via one of the following variants: |
| 84 | + |
| 85 | +You can register simple blocking throwing handlers using: |
| 86 | + |
| 87 | +```swift |
| 88 | +func register(label: String, start: @escaping () throws -> Void, shutdown: @escaping () throws -> Void) |
| 89 | + |
| 90 | +func registerShutdown(label: String, _ handler: @escaping () throws -> Void) |
| 91 | +``` |
| 92 | + |
| 93 | +or, you can register asynchronous and more complex handlers using: |
| 94 | + |
| 95 | +```swift |
| 96 | +func register(label: String, start: Handler, shutdown: Handler) |
| 97 | + |
| 98 | +func registerShutdown(label: String, _ handler: Handler) |
| 99 | +``` |
| 100 | + |
| 101 | +where `Lifecycle.Handler` is a container for an asynchronous closure defined as `(@escaping (Error?) -> Void) -> Void` |
| 102 | + |
| 103 | +`Lifecycle.Handler` comes with static helpers named `async` and `sync` designed to help simplify the registration call to: |
| 104 | + |
| 105 | +```swift |
| 106 | +let foo = ... |
| 107 | +lifecycle.register( |
| 108 | + name: "foo", |
| 109 | + start: .async(foo.asyncStart), |
| 110 | + shutdown: .async(foo.asyncShutdown) |
| 111 | +) |
| 112 | +``` |
| 113 | + |
| 114 | +or, just shutdown: |
| 115 | + |
| 116 | +```swift |
| 117 | +let foo = ... |
| 118 | +lifecycle.registerShutdown( |
| 119 | + name: "foo", |
| 120 | + .async(foo.asyncShutdown) |
| 121 | +) |
| 122 | +``` |
| 123 | + |
| 124 | + |
| 125 | +you can also register a collection of `LifecycleItem`s (less typical) using: |
| 126 | + |
| 127 | +```swift |
| 128 | +func register(_ items: [LifecycleItem]) |
| 129 | + |
| 130 | +internal func register(_ items: LifecycleItem...) |
| 131 | +``` |
| 132 | + |
| 133 | +### Starting the lifecycle |
| 134 | + |
| 135 | +Use `Lifecycle::start` function to start the application. Start handlers passed using the `register` function will be called in the order the items were registered in. |
| 136 | + |
| 137 | +`Lifecycle::start` is an asynchronous operation. If a startup error occurred, it will be logged and the startup sequence will halt on the first error, and bubble it up to the provided completion handler. |
| 138 | + |
| 139 | +```swift |
| 140 | +lifecycle.start() { error in |
| 141 | + if let error = error { |
| 142 | + logger.error("failed starting \(self) ☠️: \(error)") |
| 143 | + } else { |
| 144 | + logger.info("\(self) started successfully 🚀") |
| 145 | + } |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +`Lifecycle::start` takes optional `Lifecycle.Configuration` to further refine the `Lifecycle` behavior: |
| 150 | + |
| 151 | +* `callbackQueue`: Defines the `DispatchQueue` on which startup and shutdown handlers are executed. By default, `DispatchQueue.global` is used. |
| 152 | + |
| 153 | +* `shutdownSignal`: Defines what, if any, signals to trap for invoking shutdown. By default, `INT` and `TERM` are trapped. |
| 154 | + |
| 155 | +* `installBacktrace`: Defines if to install a crash signal trap that prints backtraces. This is especially useful for application running on Linux since Swift does not provide backtraces on Linux out of the box. This functionality is provided via the [Swift Backtrace](https://github.com/swift-server/swift-backtrace) library. |
| 156 | + |
| 157 | +### Shutdown |
| 158 | + |
| 159 | +Typical use of the library is to call on `Lifecycle::wait` after calling `Lifecycle::start`. |
| 160 | + |
| 161 | +```swift |
| 162 | +lifecycle.start() { error in |
| 163 | + ... |
| 164 | +} |
| 165 | +lifecycle.wait() // <-- blocks the thread |
| 166 | +``` |
| 167 | + |
| 168 | +If you are not interested in handling start completion, there is also a convenience method: |
| 169 | + |
| 170 | +```swift |
| 171 | +lifecycle.startAndWait() // <-- blocks the thread |
| 172 | +``` |
| 173 | + |
| 174 | +`Lifecycle::wait` and `Lifecycle::startAndWait` are blocking operations that wait for the lifecycle library to finish its shutdown sequence. |
| 175 | +The shutdown sequence is typically triggered by the `shutdownSignal` defined in the configuration. By default, `INT` and `TERM` are trapped. |
| 176 | + |
| 177 | +During shutdown, the shutdown handlers passed using the `register` or `registerShutdown` functions are called in the reverse order of the registration. E.g. |
| 178 | + |
| 179 | +``` |
| 180 | +lifecycle.register("1", ...) |
| 181 | +lifecycle.register("2", ...) |
| 182 | +lifecycle.register("3", ...) |
| 183 | +``` |
| 184 | + |
| 185 | +startup order will be 1, 2, 3 and shutdown order will be 3, 2, 1. |
| 186 | + |
| 187 | +If a shutdown error occurred, it will be logged and the shutdown sequence will *continue* to the next item, and attempt to shut it down until all registered items that have been started are shut down. |
| 188 | + |
| 189 | +In more complex cases, when signal trapping based shutdown is not appropriate, you may pass `nil` as the `shutdownSignal` configuration, and call `Lifecycle::shutdown` manually when appropriate. This is a rarely used pressure valve. `Lifecycle::shutdown` is an asynchronous operation. Errors will be logged and bubble it up to the provided completion handler. |
| 190 | + |
| 191 | +### Compatibility with SwiftNIO Futures |
| 192 | + |
| 193 | +[SwiftNIO](https://github.com/apple/swift-nio) is a popular networking library that among other things provides Future abstraction named `EventLoopFuture`. |
| 194 | + |
| 195 | +SwiftServiceLauncher comes with a compatibility module designed to make managing SwiftNIO based resources easy. |
| 196 | + |
| 197 | +Once you import `ServiceLauncherNIOCompat` module, `Lifecycle.Handler` gains a static helpers named `eventLoopFuture` designed to help simplify the registration call to: |
| 198 | + |
| 199 | +```swift |
| 200 | +let foo = ... |
| 201 | +lifecycle.register( |
| 202 | + name: "foo", |
| 203 | + start: .eventLoopFuture(foo.start), |
| 204 | + shutdown: .eventLoopFuture(foo.shutdown) |
| 205 | +) |
| 206 | +``` |
| 207 | + |
| 208 | +------- |
| 209 | + |
| 210 | + |
| 211 | +Do not hesitate to get in touch as well, over on https://forums.swift.org/c/server. |
0 commit comments