Skip to content

Commit 2523627

Browse files
authored
more robust readme (#27)
motivation: prepare to OSS changes: detailed readme
1 parent af5fb96 commit 2523627

File tree

1 file changed

+181
-3
lines changed

1 file changed

+181
-3
lines changed

README.md

Lines changed: 181 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,211 @@
11
# SwiftServiceLauncher
22

33
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`.
55

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.
78

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
930

1031
```swift
32+
// import the package
33+
import ServiceLauncher
34+
35+
// initialize the lifecycle container
1136
var lifecycle = Lifecycle()
1237

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
1341
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
1442
lifecycle.registerShutdown(
1543
name: "eventLoopGroup",
1644
eventLoopGroup.syncShutdownGracefully
1745
)
1846

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
1950
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup))
2051
lifecycle.registerShutdown(
2152
name: "HTTPClient",
2253
httpClient.syncShutdown
2354
)
2455

56+
// start the application
57+
// start handlers passed using the `register` function
58+
// will be called in the order the items were registered in
2559
lifecycle.start() { error in
60+
// this is the start completion handler.
61+
// if an error occurred you can log it here
2662
if let error = error {
2763
logger.error("failed starting \(self) ☠️: \(error)")
2864
} else {
2965
logger.info("\(self) started successfully 🚀")
3066
}
3167
}
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
3274
lifecycle.wait()
3375
```
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

Comments
 (0)