A couple of weeks ago we tagged a new 2.0.0 alpha version of Swift Service
Lifecycle which fully embraces Swift Structured Concurrency. Service Lifecycle's goal is to provide a library that helps applications to manage their internal subsystems in a structured way.
Structured Concurrency
Structured Concurrency was introduced with SE-0304 and enables developers to organise their code into high-level tasks and their child tasks. Using Structured Concurrency allows information to flow up and down the task tree such as task priority, task cancellation or user provided values. Furthermore, Structured Concurrency brings compile and runtime guarantees which make sure that every child task is finished at the end of a scope. These features make Structured Concurrency an ideal system for building complex systems such as server applications while keeping the code readability and maintainability high.
New Lifecycle APIs
Most applications are composed of multiple internal systems that make up their business logic. Before Swift Structured Concurrency was introduced ServiceLifcycle
provided APIs to manage the startup and shutdown sequences of these internal systems. ServiceLifecycle
APIs were focused on running startup and shutdown work in an ordered form. With the introduction of Structured Concurrency this can now be expressed in plain Swift code ; Therefore, we worked on a new version of ServiceLifecycle that leverages Structured Concurrency to orchestrate the service’s internal systems. At its core, the new APIs consist of a protocol called
Service
and an actor called ServiceGroup
. The former is used to define a common API to represent a subsystem. The latter is then taking a group of services and manages their lifecycle in a structure way, in practice, running each of them in a separate Swift child task using a TaskGroup
.
Example
import Lifecycle
actor FooService: Service {
func run() async throws {
print("FooService starting")
try await Task.sleep(for: .seconds(10))
print("FooService done")
}
}
actor BarService: Service {
func run() async throws {
print("BarService starting")
try await Task.sleep(for: .seconds(10))
print("BarService done")
}
}
@main
struct Application {
static func main() async throws {
let fooService = FooService()
let barService = BarService()
let serviceGroup = ServiceGroup(
services: [fooService, barService],
configuration: .init(gracefulShutdownSignals: []),
logger: logger
)
// This spawns a new child task for each service and calls the respective run method
try await serviceGroup.run()
}
}
Graceful shutdown
In addition to orchestrating Service
s, ServiceLifecycle
introduces a new concept called graceful shutdown. Graceful shutdown is closely related to task cancellation; however, while task cancellation is required, graceful shutdown is opt-in. A common pattern with networked applications like web services is to gracefully shed load when handing a shutdown signal (most commonly SIGTERM
), then exit the process once all outstanding requests have been handled. The graceful shutdown API is designed to help with this scenario. The APIs for graceful shutdown are very similar to the task cancellation APIs and graceful shutdown propagates down the task tree in the same way as task cancellation does. Additionally, ServiceLifecycle
provides convenience APIs to trigger task cancellation on graceful shutdown.
public func withGracefulShutdownHandler<T>(
operation: () async throws -> T,
onGracefulShutdown handler: @Sendable @escaping () -> Void
)
Task.isShuttingDownGracefully
public func cancelOnGracefulShutdown<T>(
_ operation: @Sendable @escaping () async throws -> T
) async rethrows -> T?
extension AsyncSequence where Self: Sendable, Element: Sendable {
public func cancelOnGracefulShutdown() → AsyncCancelOnGracefulShutdownSequence<Self>
}
Example
actor CancellingService: Service {
func run() async throws {
try await cancenlOnGracefulShutdown {
try await Task.sleep(for: .seconds(10))
}
}
}
@main
struct Application {
static func main() async throws {
let cancellingService = CancellingService()
let serviceGroup = ServiceGroup(
services: [cancellingService],
configuration: .init(gracefulShutdownSignals: [.sigterm]),
logger: logger
)
try await serviceGroup.run()
}
}
Feedback wanted
We are excited to see how everyone is adopting these new APIs and would love to hear your feedback!