Skip to content

Commit

Permalink
Rename to ServiceGroup
Browse files Browse the repository at this point in the history
  • Loading branch information
FranzBusch committed Apr 25, 2023
1 parent b6834b3 commit c9f8424
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 118 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 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 ``ServiceRunner`` actor.
them in an application easier. To achieve this ``ServiceLifecycle`` is providing the ``ServiceGroup`` actor.

## Why do we need this?

Expand All @@ -16,25 +16,25 @@ Swift introduced Structured Concurrency which already helps tremendously with ru
async 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 ``ServiceRunner`` which is responsible for orchestrating
a common API. Additionally, it provides the ``ServiceGroup`` which is responsible for orchestrating
all services in an application.

## Adopting the ServiceRunner in your application
## Adopting the ServiceGroup in your application

This article is focusing on how the ``ServiceRunner`` works and how you can adopt it 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 ServiceRunner working?
### How is the ServiceGroup working?

The ``ServiceRunner`` is just a slightly complicated task group under the hood that runs each service
in a separate child task. Furthermore, the ``ServiceRunner`` handles individual services exiting
The ``ServiceGroup`` is just a slightly complicated task group under the hood that runs each service
in a separate child task. Furthermore, the ``ServiceGroup`` handles individual services exiting
or throwing unexpectedly. 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.

### How to use the ServiceRunner?
### How to use the ServiceGroup?

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

```swift
Expand All @@ -54,8 +54,8 @@ public struct BarService: Service {
```

The `BarService` is depending in our example on the `FooService`. A dependency between services
is quite common and the ``ServiceRunner`` is inferring the dependencies from the order of the
services passed to the ``ServiceRunner/init(services:configuration:logger:)``. Services with a higher
is quite common and the ``ServiceGroup`` is inferring the dependencies from the order of the
services passed to the ``ServiceGroup/init(services:configuration:logger:)``. 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`.

Expand All @@ -66,25 +66,25 @@ struct Application {
let fooService = FooServer()
let barService = BarService(fooService: fooService)

let serviceRunner = ServiceRunner(
let serviceGroup = ServiceGroup(
// We are encoding the dependency hierarchy here by listing the fooService first
services: [fooService, barService],
configuration: .init(gracefulShutdownSignals: []),
logger: logger
)

try await serviceRunner.run()
try await serviceGroup.run()
}
}
```

### Graceful shutdown

The ``ServiceRunner`` supports graceful shutdown by taking an array of `UnixSignal`s that trigger
The ``ServiceGroup`` supports graceful shutdown by taking an array of `UnixSignal`s that trigger
the shutdown. Commonly `SIGTERM` is used to indicate graceful shutdowns in container environments
such as Docker or Kubernetes. The ``ServiceRunner`` is then gracefully shutting down each service
such as Docker or Kubernetes. The ``ServiceGroup`` is then gracefully shutting down each service
one by one in the reverse order of the array passed to the init.
Importantly, the ``ServiceRunner`` is going to wait for the ``Service/run()`` method to return
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.
Expand Down Expand Up @@ -125,13 +125,13 @@ struct Application {
}
})

let serviceRunner = ServiceRunner(
let serviceGroup = ServiceGroup(
services: [streamingService],
configuration: .init(gracefulShutdownSignals: [.sigterm]),
logger: logger
)

try await serviceRunner.run()
try await serviceGroup.run()
}
}
```
Expand Down Expand Up @@ -182,13 +182,13 @@ struct Application {
}
})

let serviceRunner = ServiceRunner(
let serviceGroup = ServiceGroup(
services: [streamingService],
configuration: .init(gracefulShutdownSignals: [.sigterm]),
logger: logger
)

try await serviceRunner.run()
try await serviceGroup.run()
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ 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 ``ServiceRunner`` cancelling all other services by cancelling the task that is running
lead to the ``ServiceGroup`` cancelling all other services by cancelling the task that is running
their respective `run()` method.

### 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 ``ServiceRunner`` is using task
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.
Expand Down
13 changes: 7 additions & 6 deletions Sources/ServiceLifecycle/Docs.docc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 ``ServiceRunner``, that handles
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
Expand All @@ -20,7 +20,7 @@ to their business logic if and how to do that.

``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
should use the ``ServiceRunner`` to orchestrate all their services.
should use the ``ServiceGroup`` to orchestrate all their services.

## Topics

Expand All @@ -33,15 +33,16 @@ should use the ``ServiceRunner`` to orchestrate all their services.

- ``Service``

### Service Runner
### Service Group

- ``ServiceRunner``
- ``ServiceRunnerConfiguration``
- ``ServiceGroup``
- ``ServiceGroupConfiguration``

### Graceful Shutdown

- ``withGracefulShutdownHandler(operation:onGracefulShutdown:)``
- ``cancelOnGracefulShutdown(_:)``

### Errors

- ``ServiceRunnerError``
- ``ServiceGroupError``
2 changes: 1 addition & 1 deletion Sources/ServiceLifecycle/GracefulShutdown.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import ConcurrencyHelpers
/// A common use-case is to listen to graceful shutdown and use the `ServerQuiescingHelper` from `swift-nio-extras` to
/// trigger the quiescing sequence. Furthermore, graceful shutdown will propagate to any child task that is currently executing
///
/// - Important: This method will only set up a handler if run inside ``ServiceRunner`` otherwise no graceful shutdown handler
/// - Important: This method will only set up a handler if run inside ``ServiceGroup`` otherwise no graceful shutdown handler
/// will be set up.
///
/// - Parameters:
Expand Down
4 changes: 2 additions & 2 deletions Sources/ServiceLifecycle/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@

/// This is the basic protocol that a service has to implement.
public protocol Service: Sendable {
/// This method is called when the ``ServiceRunner`` is starting all the services.
/// This method is called when the ``ServiceGroup`` is starting all the services.
///
/// Concrete implementation should execute their long running work in this method such as:
/// - Handling incoming connections and requests
/// - Background refreshes
///
/// - Important: Returning or throwing from this method is indicating a failure of the service and will cause the ``ServiceRunner``
/// - Important: Returning or throwing from this method is indicating a failure of the service and will cause the ``ServiceGroup``
/// to cancel the child tasks of all other running services.
func run() async throws
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,39 @@
import Logging
import UnixSignals

/// A ``ServiceRunner`` is responsible for running a number of services, setting up signal handling and signalling graceful shutdown to the services.
public actor ServiceRunner: Sendable {
/// The internal state of the ``ServiceRunner``.
/// A ``ServiceGroup`` is responsible for running a number of services, setting up signal handling and signalling graceful shutdown to the services.
public actor ServiceGroup: Sendable {
/// The internal state of the ``ServiceGroup``.
private enum State {
/// The initial state of the runner.
/// The initial state of the group.
case initial
/// The state once ``ServiceRunner/run()`` has been called.
/// The state once ``ServiceGroup/run()`` has been called.
case running(
gracefulShutdownStreamContinuation: AsyncStream<Void>.Continuation
)
/// The state once ``ServiceRunner/run()`` has finished.
/// The state once ``ServiceGroup/run()`` has finished.
case finished
}

/// The services to run.
private let services: [any Service]
/// The runner's configuration.
private let configuration: ServiceRunnerConfiguration
/// The group's configuration.
private let configuration: ServiceGroupConfiguration
/// The logger.
private let logger: Logger

/// The current state of the runner.
/// The current state of the group.
private var state: State = .initial

/// Initializes a new ``ServiceRunner``.
/// Initializes a new ``ServiceGroup``.
///
/// - Parameters:
/// - services: The services to run.
/// - configuration: The runner's configuration.
/// - configuration: The group's configuration.
/// - logger: The logger.
public init(
services: [any Service],
configuration: ServiceRunnerConfiguration,
configuration: ServiceGroupConfiguration,
logger: Logger
) {
self.services = services
Expand Down Expand Up @@ -81,7 +81,7 @@ public actor ServiceRunner: Sendable {

switch self.state {
case .initial, .finished:
fatalError("ServiceRunner is in an invalid state \(self.state)")
fatalError("ServiceGroup is in an invalid state \(self.state)")

case .running:
self.state = .finished
Expand All @@ -92,10 +92,10 @@ public actor ServiceRunner: Sendable {
}

case .running:
throw ServiceRunnerError.alreadyRunning(file: file, line: line)
throw ServiceGroupError.alreadyRunning(file: file, line: line)

case .finished:
throw ServiceRunnerError.alreadyFinished(file: file, line: line)
throw ServiceGroupError.alreadyFinished(file: file, line: line)
}
}

Expand Down Expand Up @@ -228,7 +228,7 @@ public actor ServiceRunner: Sendable {
)

group.cancelAll()
return .failure(ServiceRunnerError.serviceFinishedUnexpectedly())
return .failure(ServiceGroupError.serviceFinishedUnexpectedly())

case .serviceThrew(let service, _, let error):
// One of the servers threw an error. We have to cancel everything else now.
Expand Down Expand Up @@ -339,7 +339,7 @@ public actor ServiceRunner: Sendable {
)

group.cancelAll()
throw ServiceRunnerError.serviceFinishedUnexpectedly()
throw ServiceGroupError.serviceFinishedUnexpectedly()
}

case .serviceThrew(let service, _, let error):
Expand Down
14 changes: 7 additions & 7 deletions Sources/ServiceLifecycle/ServiceRunnerConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

import UnixSignals

/// The configuration for the ``ServiceRunner``.
public struct ServiceRunnerConfiguration: Hashable, Sendable {
/// The runner's logging configuration.
/// The configuration for the ``ServiceGroup``.
public struct ServiceGroupConfiguration: Hashable, Sendable {
/// The group's logging configuration.
public struct LoggingConfiguration: Hashable, Sendable {
public struct Keys: Hashable, Sendable {
/// The logging key used for logging the unix signal.
Expand All @@ -30,24 +30,24 @@ public struct ServiceRunnerConfiguration: Hashable, Sendable {
/// The logging key used for logging an error.
public var errorKey = "error"

/// Initializes a new ``ServiceRunnerConfiguration/LoggingConfiguration/Keys``.
/// Initializes a new ``ServiceGroupConfiguration/LoggingConfiguration/Keys``.
public init() {}
}

/// The keys used for logging.
public var keys = Keys()

/// Initializes a new ``ServiceRunnerConfiguration/LoggingConfiguration``.
/// Initializes a new ``ServiceGroupConfiguration/LoggingConfiguration``.
public init() {}
}

/// The signals that lead to graceful shutdown.
public var gracefulShutdownSignals: [UnixSignal]

/// The runner's logging configuration.
/// The group's logging configuration.
public var logging: LoggingConfiguration

/// Initializes a new ``ServiceRunnerConfiguration``.
/// Initializes a new ``ServiceGroupConfiguration``.
///
/// - Parameter gracefulShutdownSignals: The signals that lead to graceful shutdown.
public init(gracefulShutdownSignals: [UnixSignal]) {
Expand Down
20 changes: 10 additions & 10 deletions Sources/ServiceLifecycle/ServiceRunnerError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
//
//===----------------------------------------------------------------------===//

/// Errors thrown by the ``ServiceRunner``.
public struct ServiceRunnerError: Error, Hashable, Sendable {
/// Errors thrown by the ``ServiceGroup``.
public struct ServiceGroupError: Error, Hashable, Sendable {
/// A struct representing the possible error codes.
public struct Code: Hashable, Sendable, CustomStringConvertible {
private enum _Code: Hashable, Sendable {
Expand All @@ -31,17 +31,17 @@ public struct ServiceRunnerError: Error, Hashable, Sendable {
public var description: String {
switch self.code {
case .alreadyRunning:
return "The service runner is already running the services."
return "The service group is already running the services."
case .alreadyFinished:
return "The service runner has already finished running the services."
return "The service group has already finished running the services."
case .serviceFinishedUnexpectedly:
return "A service has finished unexpectedly."
}
}

/// Indicates that the service runner is already running.
/// Indicates that the service group is already running.
public static let alreadyRunning = Code(code: .alreadyRunning)
/// Indicates that the service runner has already finished running.
/// Indicates that the service group has already finished running.
public static let alreadyFinished = Code(code: .alreadyFinished)
/// Indicates that a service finished unexpectedly.
public static let serviceFinishedUnexpectedly = Code(code: .serviceFinishedUnexpectedly)
Expand Down Expand Up @@ -82,7 +82,7 @@ public struct ServiceRunnerError: Error, Hashable, Sendable {
self.backing = backing
}

/// Indicates that the service runner is already running.
/// Indicates that the service group is already running.
public static func alreadyRunning(file: String = #fileID, line: Int = #line) -> Self {
Self(
.init(
Expand All @@ -93,7 +93,7 @@ public struct ServiceRunnerError: Error, Hashable, Sendable {
)
}

/// Indicates that the service runner has already finished running.
/// Indicates that the service group has already finished running.
public static func alreadyFinished(file: String = #fileID, line: Int = #line) -> Self {
Self(
.init(
Expand All @@ -116,8 +116,8 @@ public struct ServiceRunnerError: Error, Hashable, Sendable {
}
}

extension ServiceRunnerError: CustomStringConvertible {
extension ServiceGroupError: CustomStringConvertible {
public var description: String {
"ServiceRunnerError: errorCode: \(self.backing.errorCode), file: \(self.backing.file), line: \(self.backing.line)"
"ServiceGroupError: errorCode: \(self.backing.errorCode), file: \(self.backing.file), line: \(self.backing.line)"
}
}
Loading

0 comments on commit c9f8424

Please sign in to comment.