From 24ea9176ab4531939fde168da4ff52f1b99e077d Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Wed, 8 Jan 2025 13:12:13 +0000 Subject: [PATCH] Update documentation (#195) --- README.md | 31 +-- ... adopt ServiceLifecycle in applications.md | 190 +++++++++--------- ... to adopt ServiceLifecycle in libraries.md | 149 +++++++------- Sources/ServiceLifecycle/Docs.docc/index.md | 31 ++- 4 files changed, 195 insertions(+), 206 deletions(-) diff --git a/README.md b/README.md index 6fdd306..ff1fc77 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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") @@ -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( @@ -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 diff --git a/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in applications.md b/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in applications.md index 7f747e1..015d5c7 100644 --- a/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in applications.md +++ b/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in applications.md @@ -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: -. - -### 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: . + +### 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 { @@ -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 @@ -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 ) @@ -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 @@ -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 @@ -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 diff --git a/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in libraries.md b/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in libraries.md index 79eec4e..3f8aa26 100644 --- a/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in libraries.md +++ b/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in libraries.md @@ -1,42 +1,38 @@ # How to adopt ServiceLifecycle in libraries -``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 ``Service`` protocol. +``ServiceLifecycle`` provides a unified API for services to streamline their +orchestration in libraries: the ``Service`` protocol. ## Why do we need this? Before diving into how to adopt this protocol in your library, let's take a step -back and talk about why we even need to have this unified API. A common need for -services is to either schedule long running work like sending keep alive pings -in the background or to handle new incoming work like handling new TCP -connections. Before Concurrency was introduced services put their work into -separate threads using things like `DispatchQueue`s or NIO `EventLoop`s. This -often required explicit lifetime management of the services to make sure its -resources, e.g. threads, are shutdown correctly. With the introduction of -Concurrency, specifically Structured Concurrency, we now have a better way to -structure our programs and model our work as a tree of tasks. The ``Service`` -protocol is providing a common interface that requires a single `run()` method -where services can put their long running work in. Having all services in an -application conform to this protocol enables easy orchestration of them and -makes sure they interact nicely with each other. +back and talk about why we even need to have this unified API. 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. Before +Concurrency was introduced, services put their work into separate threads using +`DispatchQueue`s or NIO `EventLoop`s. Services often required explicit lifetime +management to make sure their resources, e.g. threads, were shutdown correctly. +With the introduction of Concurrency, specifically Structured Concurrency, we +have better tools to structure our programs and model our work as a tree of +tasks. The ``Service`` protocol provides a common interface, a single `run()` +method, for services to put their long running work into. If all services in an +application conform to this protocol, then their orchestration and interaction +with each other becomes trivial. ## Adopting the Service protocol in your service -Adopting the ``Service`` protocol is quite easy in your services. The protocol -has only a single requirement which is the ``Service/run()`` method. There are a -few important caveats to it which we are going over in the next sections. Make -sure that your service is following those. +Adopting the ``Service`` protocol is quite easy. The protocol's single +requirement is the ``Service/run()`` method. Make sure that your service adheres +to the important caveats that we are going to address in the following sections. -### Make sure to use Structured Concurrency +### Use Structured Concurrency Swift offers multiple ways to use Structured Concurrency. The primary primitives are the `async` and `await` keywords which enable straight-line code to make -asynchronous calls. Furthermore, the language provides the concept of task -groups which allow the creation of concurrent work while still staying tied to -the parent task. On the other hand, Swift also provides -`Task(priority:operation:)` and `Task.detached(priority:operation:)` which -create a new unstructured Task. +asynchronous calls. The language also provides the concept of task groups: they +allow the creation of concurrent work, while staying tied to the parent task. At +the same time, Swift also provides `Task(priority:operation:)` and +`Task.detached(priority:operation:)` which create a new unstructured Task. Imagine our library wants to offer a simple `TCPEchoClient`. To make it interesting let's assume we need to send keep-alive pings on every open @@ -57,19 +53,19 @@ public actor TCPEchoClient { } ``` -The above code has a few problems. First, we are never canceling the `Task` that -is running the keep-alive pings. To do this we would need to store the `Task` in -our actor and cancel it at the appropriate time. Secondly, we actually would -need to expose a `cancel()` method on the actor to cancel the `Task`. At this -point, we have just reinvented Structured Concurrency. To avoid all of these -problems we can just conform to the ``Service`` protocol which requires a -`run()` method. This requirement already guides us to implement the long running -work inside the `run()` method. Having this method allows the user of the client -to decide in which task to schedule the keep-alive pings. They can still decide -to create an unstructured `Task` for this, but that is up to the user now. -Furthermore, we now get automatic cancellation propagation from the task that -called our `run()` method. Below is an overhauled implementation that exposes -such a `run()` method. +The above code has a few problems. First, we never cancel the `Task` that runs +the keep-alive pings. To do this, we would need to store the `Task` in our actor +and cancel it at the appropriate time. Second, we would also need to expose a +`cancel()` method on the actor to cancel the `Task`. If we were to do this, we +would have reinvented Structured Concurrency. + +To avoid all of these problems, we can conform to the ``Service`` protocol. Its +requirement guides us to implement the long running work inside the `run()` +method. It allows the user of the client to decide in which task to schedule the +keep-alive pings—unstructured `Task` are an option as well. Furthermore, we now +benefit from automatic cancellation propagation by the task that called our +`run()` method. Below you can find an overhauled implementation exposing such a +`run()` method. ```swift public actor TCPEchoClient: Service { @@ -85,58 +81,59 @@ public actor TCPEchoClient: Service { } ``` - ### Returning from your `run()` method Since the `run()` method contains long running work, returning from it is seen -as a failure and will lead to the ``ServiceGroup`` cancelling all other services -by cancelling the task that is running their respective `run()` method, unless -specified otherwise in ``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` -and ``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``. +as a failure and will lead to the ``ServiceGroup`` canceling all other services. +Unless specified otherwise in +``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` +and +``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``, +each task started in the respective `run()` method will be canceled. ### Cancellation Structured Concurrency propagates task cancellation down the task tree. Every task in the tree can check for cancellation or react to it with cancellation -handlers. The ``ServiceGroup`` is using task cancellation to tear everything -down in the case of an early return or thrown error from the `run()` method of -any of the services. Hence it is important that each service properly implements -task cancellation in their `run()` methods. +handlers. ``ServiceGroup`` uses task cancellation to tear down everything when a +services' `run() method` returns early or throws an error. Hence it is important +that each service properly implements task cancellation in their `run()` +methods. -Note: If your `run()` method is only calling other async methods that support -cancellation themselves or is consuming an `AsyncSequence`, you don't have to do -anything explicitly here. Looking at the `TCPEchoClient` example from above we -can see that we only call `Task.sleep` in our `run()` method which is supporting -task cancellation. +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. The latter is shown in the `TCPEchoClient` example above. ### Graceful shutdown -When running an application in a real environment it is often required to -gracefully shutdown the application. For example, the application might be -running in Kubernetes and a new version of it got deployed. In this case, -Kubernetes is going to send a `SIGTERM` signal to the application and expects it -to terminate within a grace period. If the application isn't stopping in time -then Kubernetes will send the `SIGKILL` signal and forcefully terminate the -process. For this reason ``ServiceLifecycle`` introduces a new _shutdown -gracefully_ concept that allows terminating the work in a structured and -graceful manner. This works similarly to task cancellation but it is fully -opt-in and up to the business logic of the application to decide what to do. +Applications are often required to be shutdown gracefully when run in a real +production environment. + +For example, the application might be deployed on Kubernetes and a new version +got released. During a rollout of that new version, Kubernetes is going to send +a `SIGTERM` signal to the application, expecting it to terminate within a grace +period. If the application does not stop in time, then Kubernetes will send the +`SIGKILL` signal and forcefully terminate the process. For this reason +``ServiceLifecycle`` introduces the _shutdown gracefully_ concept that allows +terminating an applications' work in a structured and graceful manner. 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. ``ServiceLifecycle`` exposes one free function called ``withGracefulShutdownHandler(operation:onGracefulShutdown:)`` that works similarly to the `withTaskCancellationHandler` function from the Concurrency library. Library authors are expected to make sure that any work they spawn from the `run()` method properly supports graceful shutdown. For example, a server -might be closing its listening socket to stop accepting new connections. -Importantly here though is that the server is not force closing the currently -open ones. Rather it expects the business logic on these connections to handle -graceful shutdown on their own. +might close its listening socket to stop accepting new connections. It is of +upmost importance that the server does not force the closure of any currently +open connections. It is expected that the business logic behind these +connections handles the graceful shutdown. -An example implementation of a `TCPEchoServer` on a high level that supports -graceful shutdown might look like this. +An example high level implementation of a `TCPEchoServer` with graceful shutdown +support might look like this. ```swift -public actor TCPEchoClient: Service { +public actor TCPEchoServer: Service { public init() { } public func run() async throws { @@ -151,10 +148,10 @@ public actor TCPEchoClient: Service { } ``` -In the case of our `TCPEchoClient`, the only reasonable thing to do is cancel -the iteration of our timer sequence when we receive the graceful shutdown -sequence. ``ServiceLifecycle`` is providing a convenience on `AsyncSequence` to -cancel on graceful shutdown. Let's take a look at how this works. +When we receive the graceful shutdown sequence, the only reasonable thing for +`TCPEchoClient` to do is cancel the iteration of the timer sequence. +``ServiceLifecycle`` provides a convenience on `AsyncSequence` to cancel on +graceful shutdown. Let's take a look at how this works. ```swift public actor TCPEchoClient: Service { @@ -170,5 +167,5 @@ public actor TCPEchoClient: Service { } ``` -As you can see in the code above, it is as simple as adding a -`cancelOnGracefulShutdown()` call. +As you can see in the code above, the additional `cancelOnGracefulShutdown()` +call takes care of any downstream cancellation. diff --git a/Sources/ServiceLifecycle/Docs.docc/index.md b/Sources/ServiceLifecycle/Docs.docc/index.md index 5569e35..cbd2cf8 100644 --- a/Sources/ServiceLifecycle/Docs.docc/index.md +++ b/Sources/ServiceLifecycle/Docs.docc/index.md @@ -4,19 +4,18 @@ A library for cleanly starting up and shutting down applications. ## Overview -Applications often have to orchestrate multiple internal services such as -clients or servers to implement their business logic. Doing this can become -tedious; especially when the APIs of the various services are not interoping nicely -with each other. This library tries to solve this issue by providing a ``Service`` protocol -that services should implement and an orchestrator, the ``ServiceGroup``, that handles -running the various services. - -This library is fully based on Swift Structured Concurrency which allows it to -safely orchestrate the individual services in separate child tasks. Furthermore, this library -complements the cooperative task cancellation from Structured Concurrency with a new mechanism called -_graceful shutdown_. Cancellation is indicating the tasks to stop their work as soon as possible -whereas _graceful shutdown_ just indicates them that they should come to an end but it is up -to their business logic if and how to do that. +Applications often have to orchestrate multiple internal services that orchestrate their business +logic, such as clients or servers. This quickly becomes tedious; especially when the APIs of +different services do not interoperate nicely with one another. This library sets out to solve the +issue by providing a ``Service`` protocol for services to implement, and an orchestrator, the +``ServiceGroup``, to handle running of various services. + +This library is fully based on Swift Structured Concurrency, allowing it to safely orchestrate the +individual services in separate child tasks. Furthermore, the library complements the cooperative +task cancellation from Structured Concurrency with a new mechanism called _graceful shutdown_. While +cancellation is a signal for a task to stop its work as soon as possible, _graceful shutdown_ +indicates a task that it should eventually shutdown, but it is up to the underlying business logic +to decide if and when that happens. ``ServiceLifecycle`` should be used by both library and application authors to create a seamless experience. Library authors should conform their services to the ``Service`` protocol and application authors @@ -24,7 +23,7 @@ should use the ``ServiceGroup`` to orchestrate all their services. ## 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. +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, you should use Swift Service Lifecycle. Below, you will find information to get started. ### Adding the dependency @@ -34,7 +33,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.3.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") @@ -43,7 +42,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(