1
1
# How to adopt ServiceLifecycle in applications
2
2
3
- `` ServiceLifecycle `` aims to provide a unified API that services should adopt to make orchestrating
4
- them in an application easier. To achieve this `` ServiceLifecycle `` is providing the `` ServiceGroup `` actor.
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.
5
6
6
7
## Why do we need this?
7
8
8
- When building applications we often have a bunch of services that comprise the internals of the applications.
9
- These services include fundamental needs like logging or metrics. Moreover, they also include
10
- services that compromise the application's business logic such as long-running actors.
11
- Lastly, they might also include HTTP, gRPC, or similar servers that the application is exposing.
12
- One important requirement of the application is to orchestrate the various services currently during
13
- startup and shutdown. Furthermore, the application also needs to handle a single service failing.
14
-
15
- Swift introduced Structured Concurrency which already helps tremendously with running multiple
16
- async services concurrently. This can be achieved with the use of task groups. However, Structured
17
- Concurrency doesn't enforce consistent interfaces between the services, so it becomes hard to orchestrate them.
18
- This is where `` ServiceLifecycle `` comes in. It provides the `` Service `` protocol which enforces
19
- a common API. Additionally, it provides the `` ServiceGroup `` which is responsible for orchestrating
20
- all services in an application.
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 compromise 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.
21
25
22
26
## Adopting the ServiceGroup in your application
23
27
24
- This article is focusing on how the `` ServiceGroup `` works and how you can adopt it in your application.
25
- If you are interested in how to properly implement a service, go check out the article: < doc:How-to-adopt-ServiceLifecycle-in-libraries > .
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 > .
26
32
27
33
### How is the ServiceGroup working?
28
34
29
- The `` ServiceGroup `` is just a slightly complicated task group under the hood that runs each service
30
- in a separate child task. Furthermore, the `` ServiceGroup `` handles individual services exiting
31
- or throwing unexpectedly. Lastly, it also introduces a concept called graceful shutdown which allows
32
- tearing down all services in reverse order safely. Graceful shutdown is often used in server
33
- scenarios i.e. when rolling out a new version and draining traffic from the old version.
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).
34
42
35
43
### How to use the ServiceGroup?
36
44
37
- Let's take a look how the `` ServiceGroup `` can be used in an application. First, we define some
38
- fictional services.
45
+ Let's take a look how the `` ServiceGroup `` can be used in an application. First,
46
+ we define some fictional services.
39
47
40
48
``` swift
41
49
struct FooService : Service {
@@ -53,11 +61,12 @@ public struct BarService: Service {
53
61
}
54
62
```
55
63
56
- The ` BarService ` is depending in our example on the ` FooService ` . A dependency between services
57
- is quite common and the `` ServiceGroup `` is inferring the dependencies from the order of the
58
- services passed to the `` ServiceGroup/init(services:configuration:logger:) `` . Services with a higher
59
- index can depend on services with a lower index. The following example shows how this can be applied
60
- to our ` BarService ` .
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 ` .
61
70
62
71
``` swift
63
72
@main
@@ -68,9 +77,13 @@ struct Application {
68
77
69
78
let serviceGroup = ServiceGroup (
70
79
// We are encoding the dependency hierarchy here by listing the fooService first
71
- services : [fooService, barService],
72
- configuration : .init (gracefulShutdownSignals : []),
73
- logger : logger
80
+ configuration : .init (
81
+ services : [
82
+ .init (service : fooService),
83
+ .init (service : barService)
84
+ ],
85
+ logger : logger
86
+ ),
74
87
)
75
88
76
89
try await serviceGroup.run ()
@@ -80,17 +93,26 @@ struct Application {
80
93
81
94
### Graceful shutdown
82
95
83
- The `` ServiceGroup `` supports graceful shutdown by taking an array of ` UnixSignal ` s that trigger
84
- the shutdown. Commonly ` SIGTERM ` is used to indicate graceful shutdowns in container environments
85
- such as Docker or Kubernetes. The `` ServiceGroup `` is then gracefully shutting down each service
86
- one by one in the reverse order of the array passed to the init.
87
- Importantly, the `` ServiceGroup `` is going to wait for the `` Service/run() `` method to return
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.
103
+
104
+ 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
88
108
before triggering the graceful shutdown on the next service.
89
109
90
- Since graceful shutdown is up to the individual services and application it requires explicit support.
91
- We recommend that every service author makes sure their implementation is handling graceful shutdown
92
- correctly. Lastly, application authors also have to make sure they are handling graceful shutdown.
93
- A common example of this is for applications that implement streaming behaviours.
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.
94
116
95
117
``` swift
96
118
struct StreamingService : Service {
@@ -126,27 +148,32 @@ struct Application {
126
148
})
127
149
128
150
let serviceGroup = ServiceGroup (
129
- services : [streamingService],
130
- configuration : .init (gracefulShutdownSignals : [.sigterm ]),
131
- logger : logger
151
+ configuration : .init (
152
+ services : [.init (service : streamingService)],
153
+ gracefulShutdownSignals : [.sigterm ],
154
+ logger : logger
155
+ )
132
156
)
133
157
134
158
try await serviceGroup.run ()
135
159
}
136
160
}
137
161
```
138
162
139
- The code above demonstrates a hypothetical ` StreamingService ` with a configurable handler that
140
- is invoked per stream. Each stream is handled in a separate child task concurrently.
141
- The above code doesn't support graceful shutdown right now. There are two places where we are missing it.
142
- First, the service's ` run() ` method is iterating the ` makeStream() ` async sequence. This iteration is
143
- not stopped on graceful shutdown and we are continuing to accept new streams. Furthermore,
144
- the ` streamHandler ` that we pass in our main method is also not supporting graceful shutdown since it
145
- is iterating over the incoming requests.
146
-
147
- Luckily, adding support in both places is trivial with the helpers that `` ServiceLifecycle `` exposes.
148
- In both cases, we are iterating an async sequence and what we want to do is stop the iteration.
149
- To do this we can use the ` cancelOnGracefulShutdown() ` method that `` ServiceLifecycle `` adds to
163
+ The code above demonstrates a hypothetical ` StreamingService ` with a
164
+ configurable handler that is invoked per stream. Each stream is handled in a
165
+ separate child task concurrently. The above code doesn't support graceful
166
+ shutdown right now. There are two places where we are missing it. First, the
167
+ service's ` run() ` method is iterating the ` makeStream() ` async sequence. This
168
+ iteration is not stopped on graceful shutdown and we are continuing to accept
169
+ new streams. Furthermore, the ` streamHandler ` that we pass in our main method is
170
+ also not supporting graceful shutdown since it is iterating over the incoming
171
+ requests.
172
+
173
+ Luckily, adding support in both places is trivial with the helpers that
174
+ `` ServiceLifecycle `` exposes. In both cases, we are iterating an async sequence
175
+ and what we want to do is stop the iteration. To do this we can use the
176
+ ` cancelOnGracefulShutdown() ` method that `` ServiceLifecycle `` adds to
150
177
` AsyncSequence ` . The updated code looks like this:
151
178
152
179
``` swift
@@ -183,18 +210,64 @@ struct Application {
183
210
})
184
211
185
212
let serviceGroup = ServiceGroup (
186
- services : [streamingService],
187
- configuration : .init (gracefulShutdownSignals : [.sigterm ]),
188
- logger : logger
213
+ configuration : .init (
214
+ services : [.init (service : streamingService)],
215
+ gracefulShutdownSignals : [.sigterm ],
216
+ logger : logger
217
+ )
189
218
)
190
219
191
220
try await serviceGroup.run ()
192
221
}
193
222
}
194
223
```
195
224
196
- Now one could ask - Why aren't we using cancellation in the first place here? The problem is that
197
- cancellation is forceful and doesn't allow users to make a decision if they want to cancel or not.
198
- However, graceful shutdown is very specific to business logic often. In our case, we were fine with just
199
- stopping to handle new requests on a stream. Other applications might want to send a response indicating
200
- to the client that the server is shutting down and waiting for an acknowledgment of that message.
225
+ Now one could ask - Why aren't we using cancellation in the first place here?
226
+ The problem is that cancellation is forceful and doesn't allow users to make a
227
+ decision if they want to cancel or not. However, graceful shutdown is very
228
+ specific to business logic often. In our case, we were fine with just stopping
229
+ to handle new requests on a stream. Other applications might want to send a
230
+ response indicating to the client that the server is shutting down and waiting
231
+ for an acknowledgment of that message.
232
+
233
+ ### Customizing the behavior when a service returns or throws
234
+
235
+ By default the `` ServiceGroup `` is cancelling the whole group if the one service
236
+ returns or throws. However, in some scenarios this is totally expected e.g. when
237
+ the `` ServiceGroup `` is used in a CLI tool to orchestrate some services while a
238
+ command is handled. To customize the behavior you set the
239
+ `` ServiceGroupConfiguration/ServiceConfiguration/returnBehaviour `` and
240
+ `` ServiceGroupConfiguration/ServiceConfiguration/throwBehaviour `` . Both of them
241
+ offer three different options. The default behavior for both is
242
+ `` ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/cancelGroup `` .
243
+ You can also choose to either ignore if a service returns/throws by setting it
244
+ to `` ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/ignore ``
245
+ or trigger a graceful shutdown by setting it to
246
+ `` ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/gracefullyShutdownGroup `` .
247
+
248
+ Another example where you might want to use this is when you have a service that
249
+ should be gracefully shutdown when another service exits, e.g. you want to make
250
+ sure your telemetry service is gracefully shutdown after your HTTP server
251
+ unexpectedly threw from its ` run() ` method. This setup could look like this:
252
+
253
+ ``` swift
254
+ @main
255
+ struct Application {
256
+ static func main () async throws {
257
+ let telemetryService = TelemetryService ()
258
+ let httpServer = HTTPServer ()
259
+
260
+ let serviceGroup = ServiceGroup (
261
+ configuration : .init (
262
+ services : [
263
+ .init (service : telemetryService),
264
+ .init (service : httpServer, returnBehavior : .shutdownGracefully , throwBehavior : .shutdownGracefully )
265
+ ],
266
+ logger : logger
267
+ ),
268
+ )
269
+
270
+ try await serviceGroup.run ()
271
+ }
272
+ }
273
+ ```
0 commit comments