Skip to content

Commit 24ea917

Browse files
authored
Update documentation (#195)
1 parent c2e97cf commit 24ea917

File tree

4 files changed

+195
-206
lines changed

4 files changed

+195
-206
lines changed

README.md

+16-15
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# Swift Service Lifecycle
22

3-
Swift Service Lifecycle provides a basic mechanism to cleanly start up and shut down the application, freeing resources in order before exiting.
3+
Swift Service Lifecycle provides a basic mechanism to cleanly start up and shut down an application, freeing resources in-order before exiting.
44
It also provides a `Signal`-based shutdown hook, to shut down on signals like `TERM` or `INT`.
55

66
Swift Service Lifecycle 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. Furthermore, it integrates natively with Structured Concurrency.
7+
The library encodes 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. Furthermore, it integrates natively with Structured Concurrency.
88

99
This is the beginning of a community-driven open-source project actively seeking [contributions](CONTRIBUTING.md), be it code, documentation, or ideas. What Swift Service Lifecycle provides today is covered in the [API docs](https://swiftpackageindex.com/swift-server/swift-service-lifecycle/main/documentation/servicelifecycle), but it will continue to evolve with community input.
1010

1111
## Getting started
1212

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, Swift Service Lifecycle is a great idea. Below you will find all you need to know to get started.
13+
Swift Service Lifecycle should be used 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. Below you will find all you need to know to get started.
1414

1515
### Adding the dependency
1616

@@ -20,7 +20,7 @@ To add a dependency on the package, declare it in your `Package.swift`:
2020
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"),
2121
```
2222

23-
and to your application target, add `ServiceLifecycle` to your dependencies:
23+
and add `ServiceLifecycle` to the dependencies of your application target:
2424

2525
```swift
2626
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle")
@@ -29,7 +29,7 @@ and to your application target, add `ServiceLifecycle` to your dependencies:
2929
Example `Package.swift` file with `ServiceLifecycle` as a dependency:
3030

3131
```swift
32-
// swift-tools-version:5.9
32+
// swift-tools-version:6.0
3333
import PackageDescription
3434

3535
let package = Package(
@@ -50,16 +50,17 @@ let package = Package(
5050

5151
### Using ServiceLifecycle
5252

53-
Below is a short usage example however you can find detailed documentation on how to use ServiceLifecycle over [here](https://swiftpackageindex.com/swift-server/swift-service-lifecycle/main/documentation/servicelifecycle).
54-
55-
ServiceLifecycle consists of two main building blocks. First, the `Service` protocol and secondly
56-
the `ServiceGroup`. As a library or application developer you should model your long-running work
57-
as services that implement the `Service` protocol. The protocol only requires a single `func run() async throws`
58-
method to be implemented.
59-
Afterwards, in your application you can use the `ServiceGroup` to orchestrate multiple services.
60-
The group will spawn a child task for each service and call the respective `run` method in the child task.
61-
Furthermore, the group will setup signal listeners for the configured signals and trigger a graceful shutdown
62-
on each service.
53+
You can find a short usage example below. You can find more detailed
54+
documentation on how to use ServiceLifecycle
55+
[here](https://swiftpackageindex.com/swift-server/swift-service-lifecycle/main/documentation/servicelifecycle).
56+
57+
ServiceLifecycle consists of two main building blocks. The `Service` protocol and the `ServiceGroup`
58+
actor. As a library or application developer you should model your long-running work as services
59+
that implement the `Service` protocol. The protocol only requires the implementation of a single
60+
`func run() async throws` method. Once implemented, your application you can use the `ServiceGroup`
61+
to orchestrate multiple services. The group will spawn a child task for each service and call the
62+
respective `run` method in the child task. Furthermore, the group will setup signal listeners for
63+
the configured signals and trigger a graceful shutdown on each service.
6364

6465
```swift
6566
import ServiceLifecycle

Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in applications.md

+91-99
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,42 @@
11
# How to adopt ServiceLifecycle in applications
22

3-
``ServiceLifecycle`` aims to provide a unified API that services should adopt to
4-
make orchestrating them in an application easier. To achieve this
5-
``ServiceLifecycle`` is providing the ``ServiceGroup`` actor.
3+
``ServiceLifecycle`` provides a unified API for services to streamline their
4+
orchestration in applications: the ``ServiceGroup`` actor.
65

76
## Why do we need this?
87

9-
When building applications we often have a bunch of services that comprise the
10-
internals of the applications. These services include fundamental needs like
11-
logging or metrics. Moreover, they also include services that comprise the
12-
application's business logic such as long-running actors. Lastly, they might
13-
also include HTTP, gRPC, or similar servers that the application is exposing.
14-
One important requirement of the application is to orchestrate the various
15-
services during startup and shutdown.
16-
17-
Swift introduced Structured Concurrency which already helps tremendously with
18-
running multiple asynchronous services concurrently. This can be achieved with
19-
the use of task groups. However, Structured Concurrency doesn't enforce
20-
consistent interfaces between the services, so it becomes hard to orchestrate
21-
them. This is where ``ServiceLifecycle`` comes in. It provides the ``Service``
22-
protocol which enforces a common API. Additionally, it provides the
23-
``ServiceGroup`` which is responsible for orchestrating all services in an
24-
application.
25-
26-
## Adopting the ServiceGroup in your application
27-
28-
This article is focusing on how the ``ServiceGroup`` works and how you can adopt
29-
it in your application. If you are interested in how to properly implement a
30-
service, go check out the article:
31-
<doc:How-to-adopt-ServiceLifecycle-in-libraries>.
32-
33-
### How is the ServiceGroup working?
34-
35-
The ``ServiceGroup`` is just a complicated task group under the hood that runs
36-
each service in a separate child task. Furthermore, the ``ServiceGroup`` handles
37-
individual services exiting or throwing. Lastly, it also introduces a concept
38-
called graceful shutdown which allows tearing down all services in reverse order
39-
safely. Graceful shutdown is often used in server scenarios i.e. when rolling
40-
out a new version and draining traffic from the old version (commonly referred
41-
to as quiescing).
42-
43-
### How to use the ServiceGroup?
44-
45-
Let's take a look how the ``ServiceGroup`` can be used in an application. First,
46-
we define some fictional services.
8+
Applications often rely on fundamental observability services like logging and
9+
metrics, while long-running actors bundle the application's business logic in
10+
their services. There might also exist HTTP, gRPC, or similar servers exposed by
11+
the application. It is therefore a strong requirement for the application to
12+
orchestrate the various services during startup and shutdown.
13+
14+
With the introduction of Structured Concurrency in Swift, multiple asynchronous
15+
services can be run concurrently with task groups. However, Structured
16+
Concurrency doesn't enforce consistent interfaces between the services, and it
17+
becomes hard to orchestrate them. To solve this issue, ``ServiceLifecycle``
18+
provides the ``Service`` protocol to enforce a common API, as well as the
19+
``ServiceGroup`` actor to orchestrate all services in an application.
20+
21+
## Adopting the ServiceGroup actor in your application
22+
23+
This article focuses on how ``ServiceGroup`` works, and how you can adopt it in
24+
your application. If you are interested in how to properly implement a service,
25+
go check out the article: <doc:How-to-adopt-ServiceLifecycle-in-libraries>.
26+
27+
### How does the ServiceGroup actor work?
28+
29+
Under the hood, the ``ServiceGroup`` actor is just a complicated task group that
30+
runs each service in a separate child task, and handles individual services
31+
exiting or throwing. It also introduces the concept of graceful shutdown, which
32+
allows the safe teardown of all services in reverse order. Graceful shutdown is
33+
often used in server scenarios, i.e., when rolling out a new version and
34+
draining traffic from the old version (commonly referred to as quiescing).
35+
36+
### How to use ServiceGroup?
37+
38+
Let's take a look how ``ServiceGroup`` can be used in an application. First, we
39+
define some fictional services.
4740

4841
```swift
4942
struct FooService: Service {
@@ -61,12 +54,11 @@ public struct BarService: Service {
6154
}
6255
```
6356

64-
The `BarService` is depending in our example on the `FooService`. A dependency
65-
between services is quite common and the ``ServiceGroup`` is inferring the
66-
dependencies from the order of the services passed to the
67-
``ServiceGroup/init(configuration:)``. Services with a higher index can depend
68-
on services with a lower index. The following example shows how this can be
69-
applied to our `BarService`.
57+
In our example, `BarService` depends on `FooService`. A dependency between
58+
services is very common and ``ServiceGroup`` infers the dependencies from the
59+
order that services are passed to in ``ServiceGroup/init(configuration:)``.
60+
Services with a higher index can depend on services with a lower index. The
61+
following example shows how this can be applied to our `BarService`.
7062

7163
```swift
7264
import ServiceLifecycle
@@ -81,7 +73,7 @@ struct Application {
8173
let barService = BarService(fooService: fooService)
8274

8375
let serviceGroup = ServiceGroup(
84-
// We are encoding the dependency hierarchy here by listing the fooService first
76+
// We encode the dependency hierarchy by putting fooService first
8577
services: [fooService, barService],
8678
logger: logger
8779
)
@@ -93,26 +85,24 @@ struct Application {
9385

9486
### Graceful shutdown
9587

96-
Graceful shutdown is a concept from service lifecycle which aims to be an
97-
alternative to task cancellation that is not as forceful. Graceful shutdown
98-
rather lets the various services opt-in to supporting it. A common example of
99-
when you might want to use graceful shutdown is in containerized enviroments
100-
such as Docker or Kubernetes. In those environments, `SIGTERM` is commonly used
101-
to indicate to the application that it should shut down before a `SIGKILL` is
102-
sent.
88+
Graceful shutdown is a concept introduced in ServiceLifecycle, with the aim to
89+
be a less forceful alternative to task cancellation. Graceful shutdown allows
90+
each services to opt-in support. For example, you might want to use graceful
91+
shutdown in containerized environments such as Docker or Kubernetes. In those
92+
environments, `SIGTERM` is commonly used to indicate that the application should
93+
shutdown. If it does not, then a `SIGKILL` is sent to force a non-graceful
94+
shutdown.
10395

10496
The ``ServiceGroup`` can be setup to listen to `SIGTERM` and trigger a graceful
105-
shutdown on all its orchestrated services. It will then gracefully shut down
106-
each service one by one in reverse startup order. Importantly, the
107-
``ServiceGroup`` is going to wait for the ``Service/run()`` method to return
108-
before triggering the graceful shutdown on the next service.
109-
110-
Since graceful shutdown is up to the individual services and application it
111-
requires explicit support. We recommend that every service author makes sure
112-
their implementation is handling graceful shutdown correctly. Lastly,
113-
application authors also have to make sure they are handling graceful shutdown.
114-
A common example of this is for applications that implement streaming
115-
behaviours.
97+
shutdown on all its orchestrated services. Once the signal is received, it will
98+
gracefully shut down each service one by one in reverse startup order.
99+
Importantly, the ``ServiceGroup`` is going to wait for the ``Service/run()``
100+
method to return before triggering the graceful shutdown of the next service.
101+
102+
We recommend both application and service authors to make sure that their
103+
implementations handle graceful shutdowns correctly. The following is an example
104+
application that implements a streaming service, but does not support a graceful
105+
shutdown of either the service or application.
116106

117107
```swift
118108
import ServiceLifecycle
@@ -163,20 +153,19 @@ struct Application {
163153
}
164154
```
165155

166-
The code above demonstrates a hypothetical `StreamingService` with a
167-
configurable handler that is invoked per stream. Each stream is handled in a
168-
separate child task concurrently. The above code doesn't support graceful
169-
shutdown right now. There are two places where we are missing it. First, the
170-
service's `run()` method is iterating the `makeStream()` async sequence. This
171-
iteration is not stopped on graceful shutdown and we are continuing to accept
172-
new streams. Furthermore, the `streamHandler` that we pass in our main method is
173-
also not supporting graceful shutdown since it is iterating over the incoming
174-
requests.
156+
The code above demonstrates a hypothetical `StreamingService` with one
157+
configurable handler invoked per stream. Each stream is handled concurrently in
158+
a separate child task. The above code doesn't support graceful shutdown right
159+
now, and it has to be added in two places. First, the service's `run()` method
160+
iterates the `makeStream()` async sequence. This iteration is not stopped on a
161+
graceful shutdown, and we continue to accept new streams. Also, the
162+
`streamHandler` that we pass in our main method does not support graceful
163+
shutdown, since it iterates over the incoming requests.
175164

176165
Luckily, adding support in both places is trivial with the helpers that
177-
``ServiceLifecycle`` exposes. In both cases, we are iterating an async sequence
178-
and what we want to do is stop the iteration. To do this we can use the
179-
`cancelOnGracefulShutdown()` method that ``ServiceLifecycle`` adds to
166+
``ServiceLifecycle`` exposes. In both cases, we iterate over an async sequence
167+
and we want to stop iteration for a graceful shutdown. To do this, we can use
168+
the `cancelOnGracefulShutdown()` method that ``ServiceLifecycle`` adds to
180169
`AsyncSequence`. The updated code looks like this:
181170

182171
```swift
@@ -228,33 +217,36 @@ struct Application {
228217
}
229218
```
230219

231-
Now one could ask - Why aren't we using cancellation in the first place here?
232-
The problem is that cancellation is forceful and doesn't allow users to make a
233-
decision if they want to cancel or not. However, graceful shutdown is very
234-
specific to business logic often. In our case, we were fine with just stopping
235-
to handle new requests on a stream. Other applications might want to send a
236-
response indicating to the client that the server is shutting down and waiting
237-
for an acknowledgment of that message.
220+
A valid question to ask here is why we are not using cancellation in the first
221+
place? The problem is that cancellation is forceful and does not allow users to
222+
make a decision if they want to stop a process or not. However, a graceful
223+
shutdown is often very specific to business logic. In our case, we were fine
224+
with just stopping the handling of new requests on a stream. Other applications
225+
might want to send a response to indicate to the client that the server is
226+
shutting down, and await an acknowledgment of that message.
238227

239228
### Customizing the behavior when a service returns or throws
240229

241-
By default the ``ServiceGroup`` is cancelling the whole group if the one service
242-
returns or throws. However, in some scenarios this is totally expected e.g. when
243-
the ``ServiceGroup`` is used in a CLI tool to orchestrate some services while a
244-
command is handled. To customize the behavior you set the
245-
``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` and
246-
``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``. Both of them
247-
offer three different options. The default behavior for both is
230+
By default the ``ServiceGroup`` cancels the whole group, if one service returns
231+
or throws. However, in some scenarios this is unexpected, e.g., when the
232+
``ServiceGroup`` is used in a CLI to orchestrate some services while a command
233+
is handled. To customize the behavior you set the
234+
``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior``
235+
and
236+
``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``.
237+
Both of them offer three different options. The default behavior for both is
248238
``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/cancelGroup``.
249-
You can also choose to either ignore if a service returns/throws by setting it
250-
to ``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/ignore``
251-
or trigger a graceful shutdown by setting it to
239+
You can also choose to either ignore that a service returns/throws, by setting
240+
it to
241+
``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/ignore`` or
242+
trigger a graceful shutdown by setting it to
252243
``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/gracefullyShutdownGroup``.
253244

254-
Another example where you might want to use this is when you have a service that
255-
should be gracefully shutdown when another service exits, e.g. you want to make
256-
sure your telemetry service is gracefully shutdown after your HTTP server
257-
unexpectedly threw from its `run()` method. This setup could look like this:
245+
Another example where you might want to use customize the behavior is when you
246+
have a service that should be gracefully shutdown when another service exits.
247+
For example, you want to make sure your telemetry service is gracefully shutdown
248+
after your HTTP server unexpectedly throws from its `run()` method. This setup
249+
could look like this:
258250

259251
```swift
260252
import ServiceLifecycle

0 commit comments

Comments
 (0)