From a2313612c3a416130424bf8758e32ba70d613b0b Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Thu, 21 Aug 2025 16:56:32 -0700 Subject: [PATCH 1/3] extending the content and cleaning up grammar to use present tense and more direct phrasing --- ... adopt ServiceLifecycle in applications.md | 22 +-- ... to adopt ServiceLifecycle in libraries.md | 141 +++++++----------- Sources/ServiceLifecycle/Docs.docc/index.md | 5 +- .../ServiceLifecycle/GracefulShutdown.swift | 10 +- Sources/ServiceLifecycle/Service.swift | 9 +- Sources/ServiceLifecycle/ServiceGroup.swift | 5 +- .../ServiceGroupConfiguration.swift | 6 +- .../ServiceLifecycle/ServiceRunnerError.swift | 7 +- 8 files changed, 97 insertions(+), 108 deletions(-) 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 015d5c7..9b84059 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,7 +1,7 @@ # How to adopt ServiceLifecycle in applications -``ServiceLifecycle`` provides a unified API for services to streamline their -orchestration in applications: the ``ServiceGroup`` actor. +Service Lifecycle provides a unified API for services to streamline their +orchestration in applications: the ServiceGroup actor. ## Why do we need this? @@ -35,7 +35,7 @@ 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 +Let's take a look at how ``ServiceGroup`` can be used in an application. First, we define some fictional services. ```swift @@ -87,13 +87,13 @@ struct Application { 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 +each service 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 +The ``ServiceGroup`` can be set up to listen to `SIGTERM` and trigger a graceful 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()`` @@ -227,10 +227,10 @@ shutting down, and await an acknowledgment of that message. ### Customizing the behavior when a service returns or throws -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 +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 +is handled. To customize the behavior, you set the ``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` and ``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``. @@ -242,9 +242,9 @@ it to trigger a graceful shutdown by setting it to ``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/gracefullyShutdownGroup``. -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 +Another example where you might want to customize the behavior is when you +have a service that should be gracefully shut down when another service exits. +For example, you want to make sure your telemetry service is gracefully shut down after your HTTP server unexpectedly throws from its `run()` method. This setup could look like this: 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 3f8aa26..0335d5e 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,43 +1,35 @@ # How to adopt ServiceLifecycle in libraries -``ServiceLifecycle`` provides a unified API for services to streamline their -orchestration in libraries: the ``Service`` protocol. +Adopt the Service protocol for your service to allow a Service Group to coordinate it's operation with other services. + +Service Lifecycle provides a unified API to represent a long running task: 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. 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. +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. +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 Swift Concurrency was introduced, services put their work into separate threads using a `DispatchQueue` or an NIO `EventLoop`. +Services often required explicit lifetime management to make sure their resources, such as threads, were shut down correctly. +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. +The ``Service`` protocol provides a common interface, a single `run()` method, for services to use when they run their long-running work. +If all services in an application conform to this protocol, then orchestrating them becomes trivial. ## Adopting the Service protocol in your service -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. +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 addressed in the following sections. ### 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. 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. +Swift offers multiple ways to use Concurrency. +The primary primitives are the `async` and `await` keywords which enable straight-line code to make 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 new unstructured Tasks. -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 -connection every second. Below you can see how we could implement this using -unstructured Concurrency. +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 connection every second. +Below you can see how we could implement this using unstructured concurrency. ```swift public actor TCPEchoClient { @@ -53,19 +45,17 @@ public actor TCPEchoClient { } ``` -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. +The above code has a few problems. +First, the code never cancels the `Task` that runs the keep-alive pings. +To do this, it would need to store the `Task` in the actor and cancel it at the appropriate time. +Second, it would also need to expose a `cancel()` method on the actor to cancel the `Task`. +If it were to do all of this, it 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. +To avoid all of these problems, the code 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 — using an unstructured `Task` is an option as well. +Furthermore, we now benefit from the automatic cancellation propagation by the task that called our `run()` method. +The code below illustrates an overhauled implementation that exposes such a `run()` method: ```swift public actor TCPEchoClient: Service { @@ -83,54 +73,41 @@ 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`` canceling all other services. +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. Unless specified otherwise in ``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` and ``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``, -each task started in the respective `run()` method will be canceled. +each task started in its 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. ``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. +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. +``ServiceGroup`` uses task cancellation to tear down everything when a service’s `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 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. +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 -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 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 high level implementation of a `TCPEchoServer` with graceful shutdown -support might look like this. +Applications are often required to be shut down 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 sends a `SIGTERM` signal to the application, expecting it to terminate within a grace period. +If the application does not stop in time, then Kubernetes sends the `SIGKILL` signal and forcefully terminates the process. +For this reason, ``ServiceLifecycle`` introduces the _shutdown gracefully_ concept that allows terminating an application’s 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 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 high-level implementation of a `TCPEchoServer` with graceful shutdown +support might look like this: ```swift public actor TCPEchoServer: Service { @@ -148,10 +125,9 @@ public actor TCPEchoServer: Service { } ``` -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. +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. +``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 { @@ -167,5 +143,4 @@ public actor TCPEchoClient: Service { } ``` -As you can see in the code above, the additional `cancelOnGracefulShutdown()` -call takes care of any downstream cancellation. +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 ecd7f46..9e8275f 100644 --- a/Sources/ServiceLifecycle/Docs.docc/index.md +++ b/Sources/ServiceLifecycle/Docs.docc/index.md @@ -23,7 +23,8 @@ 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, you should use Swift Service Lifecycle. Below, you will find information to get started. +If you have a server-side Swift application or a cross-platform (such as Linux and macOS) application, and would like to manage its startup and shutdown lifecycle, use Swift Service Lifecycle. +Below, you will find information to get started. ### Adding the dependency @@ -39,7 +40,7 @@ and add `ServiceLifecycle` to the dependencies of your application target: .product(name: "ServiceLifecycle", package: "swift-service-lifecycle") ``` -Example `Package.swift` file with `ServiceLifecycle` as a dependency: +The following example `Package.swift` illustrates a package with `ServiceLifecycle` as a dependency: ```swift // swift-tools-version:6.0 diff --git a/Sources/ServiceLifecycle/GracefulShutdown.swift b/Sources/ServiceLifecycle/GracefulShutdown.swift index 91c5dfb..4d55337 100644 --- a/Sources/ServiceLifecycle/GracefulShutdown.swift +++ b/Sources/ServiceLifecycle/GracefulShutdown.swift @@ -116,11 +116,11 @@ public func withGracefulShutdownHandler( /// /// This doesn’t check for graceful shutdown, and always executes the passed operation. /// The operation executes on the calling execution context and does not suspend by itself, unless the code contained within the closure does. -/// If graceful shutdown or task cancellation occurs while the operation is running, the cancellation/graceful shutdown handler will execute +/// If graceful shutdown or task cancellation occurs while the operation is running, the cancellation/graceful shutdown handler executes /// concurrently with the operation. /// /// When `withTaskCancellationOrGracefulShutdownHandler` is used in a Task that has already been gracefully shutdown or cancelled, the -/// `onCancelOrGracefulShutdown` handler will be executed immediately before operation gets to execute. This allows the `onCancelOrGracefulShutdown` +/// `onCancelOrGracefulShutdown` handler is executed immediately before operation gets to execute. This allows the `onCancelOrGracefulShutdown` /// handler to set some external “shutdown” flag that the operation may be atomically checking for in order to avoid performing any actual work /// once the operation gets to run. /// @@ -187,7 +187,7 @@ public func withTaskCancellationOrGracefulShutdownHandler( /// Waits until graceful shutdown is triggered. /// /// This method suspends the caller until graceful shutdown is triggered. If the calling task is cancelled before -/// graceful shutdown is triggered then this method will throw a `CancellationError`. +/// graceful shutdown is triggered then this method throws a `CancellationError`. /// /// - Throws: `CancellationError` if the task is cancelled. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @@ -209,9 +209,9 @@ enum ValueOrGracefulShutdown: Sendable { case cancelled } -/// Cancels the closure when a graceful shutdown was triggered. +/// Cancels the closure when a graceful shutdown is triggered. /// -/// - Parameter operation: The actual operation. +/// - Parameter operation: The operation to cancel when a graceful shutdown is triggered. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func cancelWhenGracefulShutdown( _ operation: @Sendable @escaping () async throws -> T diff --git a/Sources/ServiceLifecycle/Service.swift b/Sources/ServiceLifecycle/Service.swift index 8c5a1b6..1ea1065 100644 --- a/Sources/ServiceLifecycle/Service.swift +++ b/Sources/ServiceLifecycle/Service.swift @@ -12,7 +12,12 @@ // //===----------------------------------------------------------------------===// -/// This is the basic protocol that a service has to implement. +/// A type that represents a long-running task. +/// +/// This is the basic protocol that a service implements. +/// ``ServiceGroup`` calls ``Service/run()`` when it starts a service. +/// The asynchronous `run` method is expected to stay running until the process is terminated. +/// When implementing a service, return or throw an error from `run` to indicate the service should stop. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public protocol Service: Sendable { /// This method is called when the ``ServiceGroup`` is starting all the services. @@ -21,7 +26,7 @@ public protocol Service: Sendable { /// - Handling incoming connections and requests /// - Background refreshes /// - /// - Important: Returning or throwing from this method indicates the service should stop and will cause the + /// - Important: Returning or throwing from this method indicates the service should stop and causes the /// ``ServiceGroup`` to follow behaviors for the child tasks of all other running services specified in /// ``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` and /// ``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``. diff --git a/Sources/ServiceLifecycle/ServiceGroup.swift b/Sources/ServiceLifecycle/ServiceGroup.swift index 2fa3e8b..780cec0 100644 --- a/Sources/ServiceLifecycle/ServiceGroup.swift +++ b/Sources/ServiceLifecycle/ServiceGroup.swift @@ -16,7 +16,9 @@ import Logging import UnixSignals import AsyncAlgorithms -/// A service group is responsible for running a number of services, setting up signal handling and signalling graceful shutdown to the services. +/// A service group is responsible for running a number of services, setting up signal handling, and signalling graceful shutdown to the services. +/// +/// Create a service group to collect your long running tasks together, and combine them with signal handling to allow for graceful shutdowns of those services. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public actor ServiceGroup: Sendable, Service { /// The internal state of the ``ServiceGroup``. @@ -145,6 +147,7 @@ public actor ServiceGroup: Sendable, Service { } /// Runs all the services by spinning up a child task per service. + /// /// Furthermore, this method sets up the correct signal handlers /// for graceful shutdown. // We normally don't use underscored attributes but we really want to use the method with diff --git a/Sources/ServiceLifecycle/ServiceGroupConfiguration.swift b/Sources/ServiceLifecycle/ServiceGroupConfiguration.swift index 20fbf3c..044c7d7 100644 --- a/Sources/ServiceLifecycle/ServiceGroupConfiguration.swift +++ b/Sources/ServiceLifecycle/ServiceGroupConfiguration.swift @@ -18,6 +18,8 @@ import UnixSignals let deprecatedLoggerLabel = "service-lifecycle-deprecated-method-logger" /// The configuration for the service group. +/// +/// A service group configuration combines a set of cancellation and graceful shutdown signals with optional durations for which it expects your services to clean up and terminate. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public struct ServiceGroupConfiguration: Sendable { /// The group's logging configuration. @@ -239,7 +241,9 @@ public struct ServiceGroupConfiguration: Sendable { self.cancellationSignals = cancellationSignals } - /// Initializes a new service group configuration. + /// Creates a new service group configuration. + /// + /// Use ``init(services:gracefulShutdownSignals:cancellationSignals:logger:)-([Service],_,_,_)`` instead. @available(*, deprecated) public init(gracefulShutdownSignals: [UnixSignal]) { self.services = [] diff --git a/Sources/ServiceLifecycle/ServiceRunnerError.swift b/Sources/ServiceLifecycle/ServiceRunnerError.swift index 48245e5..742677c 100644 --- a/Sources/ServiceLifecycle/ServiceRunnerError.swift +++ b/Sources/ServiceLifecycle/ServiceRunnerError.swift @@ -12,9 +12,9 @@ // //===----------------------------------------------------------------------===// -/// Errors thrown by the ``ServiceGroup``. +/// Errors thrown by a service group. public struct ServiceGroupError: Error, Hashable, Sendable { - /// A struct representing the possible error codes. + /// A struct that represents the possible error codes. public struct Code: Hashable, Sendable, CustomStringConvertible { private enum _Code: Hashable, Sendable { case alreadyRunning @@ -73,7 +73,7 @@ public struct ServiceGroupError: Error, Hashable, Sendable { /// The error code. /// - /// - Note: This is the only thing used for the `Equatable` and `Hashable` comparison. + /// - Note: This is the only thing used for the `Equatable` and `Hashable` comparisons for instances of `ServiceGroupError`. public var errorCode: Code { self.backing.errorCode } @@ -117,6 +117,7 @@ public struct ServiceGroupError: Error, Hashable, Sendable { } extension ServiceGroupError: CustomStringConvertible { + /// A string representation of the service group error. public var description: String { "ServiceGroupError: errorCode: \(self.backing.errorCode), file: \(self.backing.file), line: \(self.backing.line)" } From b402936909a1a5b4b3c7eddff78449381391870c Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Thu, 4 Sep 2025 17:26:37 -0700 Subject: [PATCH 2/3] fixing 6.0 docc reference error --- Sources/ServiceLifecycle/ServiceGroupConfiguration.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ServiceLifecycle/ServiceGroupConfiguration.swift b/Sources/ServiceLifecycle/ServiceGroupConfiguration.swift index 044c7d7..e382da0 100644 --- a/Sources/ServiceLifecycle/ServiceGroupConfiguration.swift +++ b/Sources/ServiceLifecycle/ServiceGroupConfiguration.swift @@ -243,7 +243,8 @@ public struct ServiceGroupConfiguration: Sendable { /// Creates a new service group configuration. /// - /// Use ``init(services:gracefulShutdownSignals:cancellationSignals:logger:)-([Service],_,_,_)`` instead. + /// Use ``init(services:gracefulShutdownSignals:cancellationSignals:logger:)-1noxs`` + /// or ``init(services:gracefulShutdownSignals:cancellationSignals:logger:)-9uhzu`` instead. @available(*, deprecated) public init(gracefulShutdownSignals: [UnixSignal]) { self.services = [] From f60b6572f40e5e1293f3fe31ed1ef317e96ef311 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Thu, 4 Sep 2025 19:11:17 -0700 Subject: [PATCH 3/3] extending abstracts for the docs, adding curation for more types --- .../AsyncCancelOnGracefulShutdownSequence.swift | 14 ++++++++++---- .../AsyncGracefulShutdownSequence.swift | 2 +- ...> Adopting ServiceLifecycle in applications.md} | 14 +++++++------- ...d => Adopting ServiceLifecycle in libraries.md} | 11 ++++++----- .../AsyncCancelOnGracefulShutdownSequence.md | 13 +++++++++++++ .../{ => curation}/LoggingConfiguration.md | 0 .../ServiceLifecycle/Docs.docc/curation/Service.md | 7 +++++++ .../{ => curation}/ServiceConfiguration.md | 0 .../Docs.docc/{ => curation}/ServiceGroup.md | 0 .../{ => curation}/ServiceGroupConfiguration.md | 0 .../Docs.docc/curation/ServiceGroupError-Code.md | 13 +++++++++++++ .../Docs.docc/curation/ServiceGroupError.md | 14 ++++++++++++++ Sources/ServiceLifecycle/Docs.docc/index.md | 4 ++-- Sources/ServiceLifecycle/Service.swift | 11 ++++++----- Sources/ServiceLifecycle/ServiceGroup.swift | 4 ++-- Sources/ServiceLifecycle/ServiceRunnerError.swift | 9 +++++---- 16 files changed, 86 insertions(+), 30 deletions(-) rename Sources/ServiceLifecycle/Docs.docc/{How to adopt ServiceLifecycle in applications.md => Adopting ServiceLifecycle in applications.md} (95%) rename Sources/ServiceLifecycle/Docs.docc/{How to adopt ServiceLifecycle in libraries.md => Adopting ServiceLifecycle in libraries.md} (96%) create mode 100644 Sources/ServiceLifecycle/Docs.docc/curation/AsyncCancelOnGracefulShutdownSequence.md rename Sources/ServiceLifecycle/Docs.docc/{ => curation}/LoggingConfiguration.md (100%) create mode 100644 Sources/ServiceLifecycle/Docs.docc/curation/Service.md rename Sources/ServiceLifecycle/Docs.docc/{ => curation}/ServiceConfiguration.md (100%) rename Sources/ServiceLifecycle/Docs.docc/{ => curation}/ServiceGroup.md (100%) rename Sources/ServiceLifecycle/Docs.docc/{ => curation}/ServiceGroupConfiguration.md (100%) create mode 100644 Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError-Code.md create mode 100644 Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError.md diff --git a/Sources/ServiceLifecycle/AsyncCancelOnGracefulShutdownSequence.swift b/Sources/ServiceLifecycle/AsyncCancelOnGracefulShutdownSequence.swift index 0144264..46a7980 100644 --- a/Sources/ServiceLifecycle/AsyncCancelOnGracefulShutdownSequence.swift +++ b/Sources/ServiceLifecycle/AsyncCancelOnGracefulShutdownSequence.swift @@ -24,7 +24,7 @@ extension AsyncSequence where Self: Sendable, Element: Sendable { } } -/// An asynchronous sequence that is cancelled once graceful shutdown has triggered. +/// An asynchronous sequence that is cancelled after graceful shutdown has triggered. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public struct AsyncCancelOnGracefulShutdownSequence: AsyncSequence, Sendable where Base.Element: Sendable { @@ -40,11 +40,14 @@ where Base.Element: Sendable { AsyncMapSequence, _ElementOrGracefulShutdown> > + /// The type that the sequence produces. public typealias Element = Base.Element @usableFromInline let _merge: Merged + /// Creates a new asynchronous sequence that cancels after graceful shutdown is triggered. + /// - Parameter base: The asynchronous sequence to wrap. @inlinable public init(base: Base) { self._merge = merge( @@ -52,12 +55,14 @@ where Base.Element: Sendable { AsyncGracefulShutdownSequence().mapNil().map { _ in .gracefulShutdown } ) } - + + /// Creates an iterator for the sequence. @inlinable public func makeAsyncIterator() -> AsyncIterator { AsyncIterator(iterator: self._merge.makeAsyncIterator()) } - + + /// An iterator for an asynchronous sequence that cancels after graceful shutdown is triggered. public struct AsyncIterator: AsyncIteratorProtocol { @usableFromInline var _iterator: Merged.AsyncIterator @@ -69,7 +74,8 @@ where Base.Element: Sendable { init(iterator: Merged.AsyncIterator) { self._iterator = iterator } - + + /// Returns the next item in the sequence, or `nil` if the sequence is finished. @inlinable public mutating func next() async rethrows -> Element? { guard !self._isFinished else { diff --git a/Sources/ServiceLifecycle/AsyncGracefulShutdownSequence.swift b/Sources/ServiceLifecycle/AsyncGracefulShutdownSequence.swift index 24ce9de..84f9d81 100644 --- a/Sources/ServiceLifecycle/AsyncGracefulShutdownSequence.swift +++ b/Sources/ServiceLifecycle/AsyncGracefulShutdownSequence.swift @@ -14,7 +14,7 @@ /// An async sequence that emits an element once graceful shutdown has been triggered. /// -/// This sequence is a broadcast async sequence and will only produce one value and then finish. +/// This sequence is a broadcast async sequence and only produces one value and then finishes. /// /// - Note: This sequence respects cancellation and thus is `throwing`. @usableFromInline diff --git a/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in applications.md b/Sources/ServiceLifecycle/Docs.docc/Adopting ServiceLifecycle in applications.md similarity index 95% rename from Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in applications.md rename to Sources/ServiceLifecycle/Docs.docc/Adopting ServiceLifecycle in applications.md index 9b84059..ad06722 100644 --- a/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in applications.md +++ b/Sources/ServiceLifecycle/Docs.docc/Adopting ServiceLifecycle in applications.md @@ -1,9 +1,9 @@ -# How to adopt ServiceLifecycle in applications +# Adopting ServiceLifecycle in applications Service Lifecycle provides a unified API for services to streamline their -orchestration in applications: the ServiceGroup actor. +orchestration in applications: the Service Group actor. -## Why do we need this? +## Overview Applications often rely on fundamental observability services like logging and metrics, while long-running actors bundle the application's business logic in @@ -13,8 +13,8 @@ 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`` +Concurrency doesn't enforce consistent interfaces between services, and it can +become 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. @@ -22,7 +22,7 @@ provides the ``Service`` protocol to enforce a common API, as well as the 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: . +go check out the article: . ### How does the ServiceGroup actor work? @@ -30,7 +30,7 @@ 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 +often used in server scenarios, for example when rolling out a new version and draining traffic from the old version (commonly referred to as quiescing). ### How to use ServiceGroup? diff --git a/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in libraries.md b/Sources/ServiceLifecycle/Docs.docc/Adopting ServiceLifecycle in libraries.md similarity index 96% rename from Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in libraries.md rename to Sources/ServiceLifecycle/Docs.docc/Adopting ServiceLifecycle in libraries.md index 0335d5e..34032fd 100644 --- a/Sources/ServiceLifecycle/Docs.docc/How to adopt ServiceLifecycle in libraries.md +++ b/Sources/ServiceLifecycle/Docs.docc/Adopting ServiceLifecycle in libraries.md @@ -1,22 +1,23 @@ -# How to adopt ServiceLifecycle in libraries +# Adopting ServiceLifecycle in libraries Adopt the Service protocol for your service to allow a Service Group to coordinate it's operation with other services. -Service Lifecycle provides a unified API to represent a long running task: the Service protocol. +## Overview -## Why do we need this? +Service Lifecycle provides a unified API to represent a long running task: the ``Service`` protocol. 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. 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 Swift Concurrency was introduced, services put their work into separate threads using a `DispatchQueue` or an NIO `EventLoop`. Services often required explicit lifetime management to make sure their resources, such as threads, were shut down correctly. + 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. -The ``Service`` protocol provides a common interface, a single `run()` method, for services to use when they run their long-running work. +The `Service` protocol provides a common interface, a single `run()` method, for services to use when they run their long-running work. If all services in an application conform to this protocol, then orchestrating them becomes trivial. ## Adopting the Service protocol in your service -Adopting the ``Service`` protocol is quite easy. +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 addressed in the following sections. diff --git a/Sources/ServiceLifecycle/Docs.docc/curation/AsyncCancelOnGracefulShutdownSequence.md b/Sources/ServiceLifecycle/Docs.docc/curation/AsyncCancelOnGracefulShutdownSequence.md new file mode 100644 index 0000000..b996ba7 --- /dev/null +++ b/Sources/ServiceLifecycle/Docs.docc/curation/AsyncCancelOnGracefulShutdownSequence.md @@ -0,0 +1,13 @@ +# ``ServiceLifecycle/AsyncCancelOnGracefulShutdownSequence`` + +## Topics + +### Creating a cancelling sequence + +- ``init(base:)`` + +### Iterating the sequence + +- ``Element`` +- ``makeAsyncIterator()`` +- ``AsyncIterator`` diff --git a/Sources/ServiceLifecycle/Docs.docc/LoggingConfiguration.md b/Sources/ServiceLifecycle/Docs.docc/curation/LoggingConfiguration.md similarity index 100% rename from Sources/ServiceLifecycle/Docs.docc/LoggingConfiguration.md rename to Sources/ServiceLifecycle/Docs.docc/curation/LoggingConfiguration.md diff --git a/Sources/ServiceLifecycle/Docs.docc/curation/Service.md b/Sources/ServiceLifecycle/Docs.docc/curation/Service.md new file mode 100644 index 0000000..3b01e60 --- /dev/null +++ b/Sources/ServiceLifecycle/Docs.docc/curation/Service.md @@ -0,0 +1,7 @@ +# ``ServiceLifecycle/Service`` + +## Topics + +### Running a service + +- ``run()`` diff --git a/Sources/ServiceLifecycle/Docs.docc/ServiceConfiguration.md b/Sources/ServiceLifecycle/Docs.docc/curation/ServiceConfiguration.md similarity index 100% rename from Sources/ServiceLifecycle/Docs.docc/ServiceConfiguration.md rename to Sources/ServiceLifecycle/Docs.docc/curation/ServiceConfiguration.md diff --git a/Sources/ServiceLifecycle/Docs.docc/ServiceGroup.md b/Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroup.md similarity index 100% rename from Sources/ServiceLifecycle/Docs.docc/ServiceGroup.md rename to Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroup.md diff --git a/Sources/ServiceLifecycle/Docs.docc/ServiceGroupConfiguration.md b/Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupConfiguration.md similarity index 100% rename from Sources/ServiceLifecycle/Docs.docc/ServiceGroupConfiguration.md rename to Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupConfiguration.md diff --git a/Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError-Code.md b/Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError-Code.md new file mode 100644 index 0000000..367bfdb --- /dev/null +++ b/Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError-Code.md @@ -0,0 +1,13 @@ +# ``ServiceLifecycle/ServiceGroupError/Code`` + +## Topics + +### Service Group Errors + +- ``serviceFinishedUnexpectedly`` +- ``alreadyRunning`` +- ``alreadyFinished`` + +### Inspecting an error + +- ``description`` diff --git a/Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError.md b/Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError.md new file mode 100644 index 0000000..a0d523a --- /dev/null +++ b/Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError.md @@ -0,0 +1,14 @@ +# ``ServiceLifecycle/ServiceGroupError`` + +## Topics + +### Inspecting the errors + +- ``errorCode`` +- ``Code`` + +### Service Group Errors + +- ``serviceFinishedUnexpectedly(file:line:)`` +- ``alreadyRunning(file:line:)`` +- ``alreadyFinished(file:line:)`` diff --git a/Sources/ServiceLifecycle/Docs.docc/index.md b/Sources/ServiceLifecycle/Docs.docc/index.md index 9e8275f..dc58a5a 100644 --- a/Sources/ServiceLifecycle/Docs.docc/index.md +++ b/Sources/ServiceLifecycle/Docs.docc/index.md @@ -66,8 +66,8 @@ let package = Package( ### Articles -- -- +- +- ### Service protocol diff --git a/Sources/ServiceLifecycle/Service.swift b/Sources/ServiceLifecycle/Service.swift index 1ea1065..e5c23c0 100644 --- a/Sources/ServiceLifecycle/Service.swift +++ b/Sources/ServiceLifecycle/Service.swift @@ -14,15 +14,16 @@ /// A type that represents a long-running task. /// -/// This is the basic protocol that a service implements. -/// ``ServiceGroup`` calls ``Service/run()`` when it starts a service. -/// The asynchronous `run` method is expected to stay running until the process is terminated. +/// This is the protocol that a service implements. +/// +/// ``ServiceGroup`` calls the asynchronous ``Service/run()`` method when it starts a service. +/// A service group expects the `run` method to stay running until the process is terminated. /// When implementing a service, return or throw an error from `run` to indicate the service should stop. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public protocol Service: Sendable { - /// This method is called when the ``ServiceGroup`` is starting all the services. + /// The service group calls this method when it starts the service. /// - /// Concrete implementation should execute their long running work in this method such as: + /// Your implementation should execute its long running work in this method such as: /// - Handling incoming connections and requests /// - Background refreshes /// diff --git a/Sources/ServiceLifecycle/ServiceGroup.swift b/Sources/ServiceLifecycle/ServiceGroup.swift index 780cec0..668eea4 100644 --- a/Sources/ServiceLifecycle/ServiceGroup.swift +++ b/Sources/ServiceLifecycle/ServiceGroup.swift @@ -49,7 +49,7 @@ public actor ServiceGroup: Sendable, Service { /// The current state of the group. private var state: State - /// Creates a service group. + /// Creates a service group from a service group configuration you provide. /// /// - Parameters: /// - configuration: The group's configuration @@ -73,7 +73,7 @@ public actor ServiceGroup: Sendable, Service { /// Creates a service group. /// /// - Parameters: - /// - services: The groups's service configurations. + /// - services: The groups's services.. /// - gracefulShutdownSignals: The signals that lead to graceful shutdown. /// - cancellationSignals: The signals that lead to cancellation. /// - logger: The group's logger. diff --git a/Sources/ServiceLifecycle/ServiceRunnerError.swift b/Sources/ServiceLifecycle/ServiceRunnerError.swift index 742677c..d77d695 100644 --- a/Sources/ServiceLifecycle/ServiceRunnerError.swift +++ b/Sources/ServiceLifecycle/ServiceRunnerError.swift @@ -27,7 +27,8 @@ public struct ServiceGroupError: Error, Hashable, Sendable { private init(code: _Code) { self.code = code } - + + /// A string representation of a service group error. public var description: String { switch self.code { case .alreadyRunning: @@ -82,7 +83,7 @@ public struct ServiceGroupError: Error, Hashable, Sendable { self.backing = backing } - /// Indicates that the service group is already running. + /// An error that indicates that the service group is already running. public static func alreadyRunning(file: String = #fileID, line: Int = #line) -> Self { Self( .init( @@ -93,7 +94,7 @@ public struct ServiceGroupError: Error, Hashable, Sendable { ) } - /// Indicates that the service group has already finished running. + /// An error that indicates that the service group has already finished running. public static func alreadyFinished(file: String = #fileID, line: Int = #line) -> Self { Self( .init( @@ -104,7 +105,7 @@ public struct ServiceGroupError: Error, Hashable, Sendable { ) } - /// Indicates that a service finished unexpectedly even though it indicated it is a long running service. + /// An error that indicates that a service finished unexpectedly even though it indicated it is a long running service. public static func serviceFinishedUnexpectedly(file: String = #fileID, line: Int = #line) -> Self { Self( .init(