Skip to content

Commit 83cecce

Browse files
authored
extending the documentation content (#212)
- cleaning up grammar to use present tense and more direct phrasing - extending documentation coverage Current state: ``` | Abstract | Curated | Code Listing Types | 71% (10/14) | 64% (9/14) | 0.0% (0/14) Members | 44% (49/111) | 77% (85/111) | 0.0% (0/111) Globals | 73% (8/11) | 91% (10/11) | 9.1% (1/11) ```
1 parent 21cf72f commit 83cecce

19 files changed

+259
-213
lines changed

Sources/ServiceLifecycle/AsyncCancelOnGracefulShutdownSequence.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ extension AsyncSequence where Self: Sendable, Element: Sendable {
2424
}
2525
}
2626

27-
/// An asynchronous sequence that is cancelled once graceful shutdown has triggered.
27+
/// An asynchronous sequence that is cancelled after graceful shutdown has triggered.
2828
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
2929
public struct AsyncCancelOnGracefulShutdownSequence<Base: AsyncSequence & Sendable>: AsyncSequence, Sendable
3030
where Base.Element: Sendable {
@@ -40,11 +40,14 @@ where Base.Element: Sendable {
4040
AsyncMapSequence<AsyncMapNilSequence<AsyncGracefulShutdownSequence>, _ElementOrGracefulShutdown>
4141
>
4242

43+
/// The type that the sequence produces.
4344
public typealias Element = Base.Element
4445

4546
@usableFromInline
4647
let _merge: Merged
4748

49+
/// Creates a new asynchronous sequence that cancels after graceful shutdown is triggered.
50+
/// - Parameter base: The asynchronous sequence to wrap.
4851
@inlinable
4952
public init(base: Base) {
5053
self._merge = merge(
@@ -53,11 +56,13 @@ where Base.Element: Sendable {
5356
)
5457
}
5558

59+
/// Creates an iterator for the sequence.
5660
@inlinable
5761
public func makeAsyncIterator() -> AsyncIterator {
5862
AsyncIterator(iterator: self._merge.makeAsyncIterator())
5963
}
6064

65+
/// An iterator for an asynchronous sequence that cancels after graceful shutdown is triggered.
6166
public struct AsyncIterator: AsyncIteratorProtocol {
6267
@usableFromInline
6368
var _iterator: Merged.AsyncIterator
@@ -70,6 +75,7 @@ where Base.Element: Sendable {
7075
self._iterator = iterator
7176
}
7277

78+
/// Returns the next item in the sequence, or `nil` if the sequence is finished.
7379
@inlinable
7480
public mutating func next() async rethrows -> Element? {
7581
guard !self._isFinished else {

Sources/ServiceLifecycle/AsyncGracefulShutdownSequence.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
/// An async sequence that emits an element once graceful shutdown has been triggered.
1616
///
17-
/// This sequence is a broadcast async sequence and will only produce one value and then finish.
17+
/// This sequence is a broadcast async sequence and only produces one value and then finishes.
1818
///
1919
/// - Note: This sequence respects cancellation and thus is `throwing`.
2020
@usableFromInline

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

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# How to adopt ServiceLifecycle in applications
1+
# Adopting ServiceLifecycle in applications
22

3-
``ServiceLifecycle`` provides a unified API for services to streamline their
4-
orchestration in applications: the ``ServiceGroup`` actor.
3+
Service Lifecycle provides a unified API for services to streamline their
4+
orchestration in applications: the Service Group actor.
55

6-
## Why do we need this?
6+
## Overview
77

88
Applications often rely on fundamental observability services like logging and
99
metrics, while long-running actors bundle the application's business logic in
@@ -13,29 +13,29 @@ orchestrate the various services during startup and shutdown.
1313

1414
With the introduction of Structured Concurrency in Swift, multiple asynchronous
1515
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``
16+
Concurrency doesn't enforce consistent interfaces between services, and it can
17+
become hard to orchestrate them. To solve this issue, ``ServiceLifecycle``
1818
provides the ``Service`` protocol to enforce a common API, as well as the
1919
``ServiceGroup`` actor to orchestrate all services in an application.
2020

2121
## Adopting the ServiceGroup actor in your application
2222

2323
This article focuses on how ``ServiceGroup`` works, and how you can adopt it in
2424
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>.
25+
go check out the article: <doc:Adopting-ServiceLifecycle-in-libraries>.
2626

2727
### How does the ServiceGroup actor work?
2828

2929
Under the hood, the ``ServiceGroup`` actor is just a complicated task group that
3030
runs each service in a separate child task, and handles individual services
3131
exiting or throwing. It also introduces the concept of graceful shutdown, which
3232
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
33+
often used in server scenarios, for example when rolling out a new version and
3434
draining traffic from the old version (commonly referred to as quiescing).
3535

3636
### How to use ServiceGroup?
3737

38-
Let's take a look how ``ServiceGroup`` can be used in an application. First, we
38+
Let's take a look at how ``ServiceGroup`` can be used in an application. First, we
3939
define some fictional services.
4040

4141
```swift
@@ -87,13 +87,13 @@ struct Application {
8787

8888
Graceful shutdown is a concept introduced in ServiceLifecycle, with the aim to
8989
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
90+
each service to opt-in support. For example, you might want to use graceful
9191
shutdown in containerized environments such as Docker or Kubernetes. In those
9292
environments, `SIGTERM` is commonly used to indicate that the application should
9393
shutdown. If it does not, then a `SIGKILL` is sent to force a non-graceful
9494
shutdown.
9595

96-
The ``ServiceGroup`` can be setup to listen to `SIGTERM` and trigger a graceful
96+
The ``ServiceGroup`` can be set up to listen to `SIGTERM` and trigger a graceful
9797
shutdown on all its orchestrated services. Once the signal is received, it will
9898
gracefully shut down each service one by one in reverse startup order.
9999
Importantly, the ``ServiceGroup`` is going to wait for the ``Service/run()``
@@ -227,10 +227,10 @@ shutting down, and await an acknowledgment of that message.
227227

228228
### Customizing the behavior when a service returns or throws
229229

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
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
232232
``ServiceGroup`` is used in a CLI to orchestrate some services while a command
233-
is handled. To customize the behavior you set the
233+
is handled. To customize the behavior, you set the
234234
``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior``
235235
and
236236
``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``.
@@ -242,9 +242,9 @@ it to
242242
trigger a graceful shutdown by setting it to
243243
``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/gracefullyShutdownGroup``.
244244

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
245+
Another example where you might want to customize the behavior is when you
246+
have a service that should be gracefully shut down when another service exits.
247+
For example, you want to make sure your telemetry service is gracefully shut down
248248
after your HTTP server unexpectedly throws from its `run()` method. This setup
249249
could look like this:
250250

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Adopting ServiceLifecycle in libraries
2+
3+
Adopt the Service protocol for your service to allow a Service Group to coordinate it's operation with other services.
4+
5+
## Overview
6+
7+
Service Lifecycle provides a unified API to represent a long running task: the ``Service`` protocol.
8+
9+
Before diving into how to adopt this protocol in your library, let's take a step back and talk about why we need to have this unified API.
10+
Services often need to schedule long-running tasks, such as sending keep-alive pings in the background, or the handling of incoming work like new TCP connections.
11+
Before Swift Concurrency was introduced, services put their work into separate threads using a `DispatchQueue` or an NIO `EventLoop`.
12+
Services often required explicit lifetime management to make sure their resources, such as threads, were shut down correctly.
13+
14+
With the introduction of Swift Concurrency, specifically by using Structured Concurrency, we have better tools to structure our programs and model our work as a tree of tasks.
15+
The `Service` protocol provides a common interface, a single `run()` method, for services to use when they run their long-running work.
16+
If all services in an application conform to this protocol, then orchestrating them becomes trivial.
17+
18+
## Adopting the Service protocol in your service
19+
20+
Adopting the `Service` protocol is quite easy.
21+
The protocol's single requirement is the ``Service/run()`` method.
22+
Make sure that your service adheres to the important caveats addressed in the following sections.
23+
24+
### Use Structured Concurrency
25+
26+
Swift offers multiple ways to use Concurrency.
27+
The primary primitives are the `async` and `await` keywords which enable straight-line code to make asynchronous calls.
28+
The language also provides the concept of task groups; they allow the creation of concurrent work, while staying tied to the parent task.
29+
At the same time, Swift also provides `Task(priority:operation:)` and `Task.detached(priority:operation:)` which create new unstructured Tasks.
30+
31+
Imagine our library wants to offer a simple `TCPEchoClient`.
32+
To make it Interesting, let's assume we need to send keep-alive pings on every open connection every second.
33+
Below you can see how we could implement this using unstructured concurrency.
34+
35+
```swift
36+
public actor TCPEchoClient {
37+
public init() {
38+
Task {
39+
for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) {
40+
self.sendKeepAlivePings()
41+
}
42+
}
43+
}
44+
45+
private func sendKeepAlivePings() async { ... }
46+
}
47+
```
48+
49+
The above code has a few problems.
50+
First, the code never cancels the `Task` that runs the keep-alive pings.
51+
To do this, it would need to store the `Task` in the actor and cancel it at the appropriate time.
52+
Second, it would also need to expose a `cancel()` method on the actor to cancel the `Task`.
53+
If it were to do all of this, it would have reinvented Structured Concurrency.
54+
55+
To avoid all of these problems, the code can conform to the ``Service`` protocol.
56+
Its requirement guides us to implement the long-running work inside the `run()` method.
57+
It allows the user of the client to decide in which task to schedule the keep-alive pings — using an unstructured `Task` is an option as well.
58+
Furthermore, we now benefit from the automatic cancellation propagation by the task that called our `run()` method.
59+
The code below illustrates an overhauled implementation that exposes such a `run()` method:
60+
61+
```swift
62+
public actor TCPEchoClient: Service {
63+
public init() { }
64+
65+
public func run() async throws {
66+
for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) {
67+
self.sendKeepAlivePings()
68+
}
69+
}
70+
71+
private func sendKeepAlivePings() async { ... }
72+
}
73+
```
74+
75+
### Returning from your `run()` method
76+
77+
Since the `run()` method contains long-running work, returning from it is interpreted as a failure and will lead to the ``ServiceGroup`` canceling all other services.
78+
Unless specified otherwise in
79+
``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior``
80+
and
81+
``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``,
82+
each task started in its respective `run()` method will be canceled.
83+
84+
### Cancellation
85+
86+
Structured Concurrency propagates task cancellation down the task tree.
87+
Every task in the tree can check for cancellation or react to it with cancellation handlers.
88+
``ServiceGroup`` uses task cancellation to tear down everything when a service’s `run()` method returns early or throws an error.
89+
Hence, it is important that each service properly implements task cancellation in their `run()` methods.
90+
91+
Note: If your `run()` method calls other async methods that support cancellation, or consumes an `AsyncSequence`, then you don't have to do anything explicitly.
92+
The latter is shown in the `TCPEchoClient` example above.
93+
94+
### Graceful shutdown
95+
96+
Applications are often required to be shut down gracefully when run in a real production environment.
97+
98+
For example, the application might be deployed on Kubernetes and a new version got released.
99+
During a rollout of that new version, Kubernetes sends a `SIGTERM` signal to the application, expecting it to terminate within a grace period.
100+
If the application does not stop in time, then Kubernetes sends the `SIGKILL` signal and forcefully terminates the process.
101+
For this reason, ``ServiceLifecycle`` introduces the _shutdown gracefully_ concept that allows terminating an application’s work in a structured and graceful manner.
102+
This behavior is similar to task cancellation, but due to its opt-in nature, it is up to the business logic of the application to decide what to do.
103+
104+
``ServiceLifecycle`` exposes one free function called ``withGracefulShutdownHandler(operation:onGracefulShutdown:)`` that works similarly to the `withTaskCancellationHandler` function from the Concurrency library.
105+
Library authors are expected to make sure that any work they spawn from the `run()` method properly supports graceful shutdown.
106+
For example, a server might close its listening socket to stop accepting new connections.
107+
It is of upmost importance that the server does not force the closure of any currently open connections.
108+
It is expected that the business logic behind these connections handles the graceful shutdown.
109+
110+
An example high-level implementation of a `TCPEchoServer` with graceful shutdown
111+
support might look like this:
112+
113+
```swift
114+
public actor TCPEchoServer: Service {
115+
public init() { }
116+
117+
public func run() async throws {
118+
await withGracefulShutdownHandler {
119+
for connection in self.listeningSocket.connections {
120+
// Handle incoming connections
121+
}
122+
} onGracefulShutdown: {
123+
self.listeningSocket.close()
124+
}
125+
}
126+
}
127+
```
128+
129+
When the above code receives the graceful shutdown sequence, the only reasonable thing for `TCPEchoClient` to do is cancel the iteration of the timer sequence.
130+
``ServiceLifecycle`` provides a convenience on `AsyncSequence` to cancel on graceful shutdown.
131+
Let's take a look at how this works.
132+
133+
```swift
134+
public actor TCPEchoClient: Service {
135+
public init() { }
136+
137+
public func run() async throws {
138+
for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous).cancelOnGracefulShutdown() {
139+
self.sendKeepAlivePings()
140+
}
141+
}
142+
143+
private func sendKeepAlivePings() async { ... }
144+
}
145+
```
146+
147+
As you can see in the code above, the additional `cancelOnGracefulShutdown()` call takes care of any downstream cancellation.

0 commit comments

Comments
 (0)