Skip to content

Commit

Permalink
Update documentation (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
mimischi authored Jan 8, 2025
1 parent c2e97cf commit 24ea917
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 206 deletions.
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Swift Service Lifecycle

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

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

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.

## Getting started

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

### Adding the dependency

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

and to your application target, add `ServiceLifecycle` to your dependencies:
and add `ServiceLifecycle` to the dependencies of your application target:

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

```swift
// swift-tools-version:5.9
// swift-tools-version:6.0
import PackageDescription

let package = Package(
Expand All @@ -50,16 +50,17 @@ let package = Package(

### Using ServiceLifecycle

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

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

ServiceLifecycle consists of two main building blocks. The `Service` protocol and the `ServiceGroup`
actor. As a library or application developer you should model your long-running work as services
that implement the `Service` protocol. The protocol only requires the implementation of a single
`func run() async throws` method. Once implemented, your application you can use the `ServiceGroup`
to orchestrate multiple services. The group will spawn a child task for each service and call the
respective `run` method in the child task. Furthermore, the group will setup signal listeners for
the configured signals and trigger a graceful shutdown on each service.

```swift
import ServiceLifecycle
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,42 @@
# How to adopt ServiceLifecycle in applications

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

## Why do we need this?

When building applications we often have a bunch of services that comprise the
internals of the applications. These services include fundamental needs like
logging or metrics. Moreover, they also include services that comprise the
application's business logic such as long-running actors. Lastly, they might
also include HTTP, gRPC, or similar servers that the application is exposing.
One important requirement of the application is to orchestrate the various
services during startup and shutdown.

Swift introduced Structured Concurrency which already helps tremendously with
running multiple asynchronous services concurrently. This can be achieved with
the use of task groups. However, Structured Concurrency doesn't enforce
consistent interfaces between the services, so it becomes hard to orchestrate
them. This is where ``ServiceLifecycle`` comes in. It provides the ``Service``
protocol which enforces a common API. Additionally, it provides the
``ServiceGroup`` which is responsible for orchestrating all services in an
application.

## Adopting the ServiceGroup in your application

This article is focusing on how the ``ServiceGroup`` works and how you can adopt
it in your application. If you are interested in how to properly implement a
service, go check out the article:
<doc:How-to-adopt-ServiceLifecycle-in-libraries>.

### How is the ServiceGroup working?

The ``ServiceGroup`` is just a complicated task group under the hood that runs
each service in a separate child task. Furthermore, the ``ServiceGroup`` handles
individual services exiting or throwing. Lastly, it also introduces a concept
called graceful shutdown which allows tearing down all services in reverse order
safely. Graceful shutdown is often used in server scenarios i.e. when rolling
out a new version and draining traffic from the old version (commonly referred
to as quiescing).

### How to use the ServiceGroup?

Let's take a look how the ``ServiceGroup`` can be used in an application. First,
we define some fictional services.
Applications often rely on fundamental observability services like logging and
metrics, while long-running actors bundle the application's business logic in
their services. There might also exist HTTP, gRPC, or similar servers exposed by
the application. It is therefore a strong requirement for the application to
orchestrate the various services during startup and shutdown.

With the introduction of Structured Concurrency in Swift, multiple asynchronous
services can be run concurrently with task groups. However, Structured
Concurrency doesn't enforce consistent interfaces between the services, and it
becomes hard to orchestrate them. To solve this issue, ``ServiceLifecycle``
provides the ``Service`` protocol to enforce a common API, as well as the
``ServiceGroup`` actor to orchestrate all services in an application.

## Adopting the ServiceGroup actor in your application

This article focuses on how ``ServiceGroup`` works, and how you can adopt it in
your application. If you are interested in how to properly implement a service,
go check out the article: <doc:How-to-adopt-ServiceLifecycle-in-libraries>.

### How does the ServiceGroup actor work?

Under the hood, the ``ServiceGroup`` actor is just a complicated task group that
runs each service in a separate child task, and handles individual services
exiting or throwing. It also introduces the concept of graceful shutdown, which
allows the safe teardown of all services in reverse order. Graceful shutdown is
often used in server scenarios, i.e., when rolling out a new version and
draining traffic from the old version (commonly referred to as quiescing).

### How to use ServiceGroup?

Let's take a look how ``ServiceGroup`` can be used in an application. First, we
define some fictional services.

```swift
struct FooService: Service {
Expand All @@ -61,12 +54,11 @@ public struct BarService: Service {
}
```

The `BarService` is depending in our example on the `FooService`. A dependency
between services is quite common and the ``ServiceGroup`` is inferring the
dependencies from the order of the services passed to the
``ServiceGroup/init(configuration:)``. Services with a higher index can depend
on services with a lower index. The following example shows how this can be
applied to our `BarService`.
In our example, `BarService` depends on `FooService`. A dependency between
services is very common and ``ServiceGroup`` infers the dependencies from the
order that services are passed to in ``ServiceGroup/init(configuration:)``.
Services with a higher index can depend on services with a lower index. The
following example shows how this can be applied to our `BarService`.

```swift
import ServiceLifecycle
Expand All @@ -81,7 +73,7 @@ struct Application {
let barService = BarService(fooService: fooService)

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

### Graceful shutdown

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

The ``ServiceGroup`` can be setup to listen to `SIGTERM` and trigger a graceful
shutdown on all its orchestrated services. It will then gracefully shut down
each service one by one in reverse startup order. Importantly, the
``ServiceGroup`` is going to wait for the ``Service/run()`` method to return
before triggering the graceful shutdown on the next service.

Since graceful shutdown is up to the individual services and application it
requires explicit support. We recommend that every service author makes sure
their implementation is handling graceful shutdown correctly. Lastly,
application authors also have to make sure they are handling graceful shutdown.
A common example of this is for applications that implement streaming
behaviours.
shutdown on all its orchestrated services. Once the signal is received, it will
gracefully shut down each service one by one in reverse startup order.
Importantly, the ``ServiceGroup`` is going to wait for the ``Service/run()``
method to return before triggering the graceful shutdown of the next service.

We recommend both application and service authors to make sure that their
implementations handle graceful shutdowns correctly. The following is an example
application that implements a streaming service, but does not support a graceful
shutdown of either the service or application.

```swift
import ServiceLifecycle
Expand Down Expand Up @@ -163,20 +153,19 @@ struct Application {
}
```

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

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

```swift
Expand Down Expand Up @@ -228,33 +217,36 @@ struct Application {
}
```

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

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

By default the ``ServiceGroup`` is cancelling the whole group if the one service
returns or throws. However, in some scenarios this is totally expected e.g. when
the ``ServiceGroup`` is used in a CLI tool to orchestrate some services while a
command is handled. To customize the behavior you set the
``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` and
``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``. Both of them
offer three different options. The default behavior for both is
By default the ``ServiceGroup`` cancels the whole group, if one service returns
or throws. However, in some scenarios this is unexpected, e.g., when the
``ServiceGroup`` is used in a CLI to orchestrate some services while a command
is handled. To customize the behavior you set the
``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior``
and
``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``.
Both of them offer three different options. The default behavior for both is
``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/cancelGroup``.
You can also choose to either ignore if a service returns/throws by setting it
to ``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/ignore``
or trigger a graceful shutdown by setting it to
You can also choose to either ignore that a service returns/throws, by setting
it to
``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/ignore`` or
trigger a graceful shutdown by setting it to
``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/gracefullyShutdownGroup``.

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

```swift
import ServiceLifecycle
Expand Down
Loading

0 comments on commit 24ea917

Please sign in to comment.