Swift Service Lifecycle

repository·main·Indexed 19 days ago

https://github.com/swift-server/swift-service-lifecycle

A non-framework specific mechanism for cleanly starting up and shutting down Swift applications, ensuring resources are freed in the correct order. It integrates natively with Swift Structured Concurrency using the Service protocol for long-running work and the ServiceGroup actor to orchestrate multiple services, manage dependency order, and handle graceful shutdown signals like SIGTERM.

Tokens
6.2K
Snippets
17
Records
27
Agent score
67%

What's inside swift-service-lifecycle

  1. Configure termination behaviors for a service

    main

    When creating a ServiceConfiguration, you can control the lifecycle of the surrounding service group using TerminationBehavior for both success and failure scenarios:

    • successTerminationBehavior: Defines what happens to the group when the service terminates normally.
    • failureTerminationBehavior: Defines what happens to the group when the service terminates with an error.
  2. How ServiceLifecycle orchestrates services

    main

    Swift Service Lifecycle provides a standardized way to manage the startup and shutdown of applications using two primary abstractions:

    1. Service protocol: Library authors should implement this protocol in their services to ensure they can be managed by an orchestrator.
    2. ServiceGroup: Application authors use this orchestrator to run multiple services. It manages the lifecycle of various services, typically running them in separate child tasks using Swift Structured Concurrency.

    The library distinguishes between Task Cancellation (a signal to stop work as soon as possible) and Graceful Shutdown (a signal that a task should eventually shut down, allowing the business logic to decide when it is safe to stop).

  3. How Service and ServiceGroup work together

    main

    Swift Service Lifecycle uses two primary abstractions to manage application startup and shutdown:

    1. Service protocol: You model long-running work by implementing this protocol. It requires a single method: func run() async throws. A service can be a struct, class, or actor.

    2. ServiceGroup actor: This orchestrates multiple services. When you call run() on a ServiceGroup, it spawns a child task for each provided service and executes their run() methods. The group also manages signal listeners (like SIGTERM) to trigger a graceful shutdown across all services when a signal is received.

    This design integrates natively with Swift's Structured Concurrency.

    import ServiceLifecycle
    import Logging
    
    // 1. Implement the Service protocol
    struct FooService: Service {
        func run() async throws {
            print("FooService starting")
            try await Task.sleep(for: .seconds(10))
            print("FooService done")
        }
    }
    
    @main
    struct Application {
        static let logger = Logger(label: "Application")
        
        static func main() async throws {
            let service1 = FooService()
            let service2 = FooService()
            
            // 2. Orchestrate services with ServiceGroup
            let serviceGroup = ServiceGroup(
                services: [service1, service2],
                gracefulShutdownSignals: [.sigterm],
                logger: logger
            )
            
            try await serviceGroup.run()
        }
    }
  4. Implementing Graceful Shutdown in a Service

    main

    Graceful shutdown is an opt-in mechanism that allows your service to perform cleanup (like closing sockets or finishing active requests) before being forcefully terminated. This is distinct from task cancellation.

    Using withGracefulShutdownHandler

    Use the withGracefulShutdownHandler(operation:onGracefulShutdown:) function to define what happens when a shutdown signal is received. The operation block contains your main logic, and the onGracefulShutdown block contains your cleanup logic.

    Using cancelOnGracefulShutdown()

    For services that rely on AsyncSequence (like timers or connection streams), ServiceLifecycle provides a convenience method .cancelOnGracefulShutdown() to automatically trigger cancellation of the sequence when a graceful shutdown occurs.

    // Example: Server with graceful shutdown
    public actor TCPEchoServer: Service {
      public init() { }
      public func run() async throws {
        await withGracefulShutdownHandler {
            for connection in self.listeningSocket.connections {
              // Handle incoming connections
            }
        } onGracefulShutdown: {
            self.listeningSocket.close()
        }
      }
    }
    
    // Example: Client using cancelOnGracefulShutdown()
    public actor TCPEchoClient: Service {
      public init() { }
      public func run() async throws {
        for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous).cancelOnGracefulShutdown() {
          self.sendKeepAlivePings()
        }
      }
      private func sendKeepAlivePings() async { ... }
    }
  5. Add ServiceLifecycle dependency to your Swift package

    main

    To use ServiceLifecycle in your project, add it to your Package.swift file. First, declare the package dependency using the GitHub URL, then add the ServiceLifecycle product to your specific target's dependencies.

    // swift-tools-version:6.0
    import PackageDescription
    
    let package = Package(
        name: "my-application",
        dependencies: [
            .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0"),
        ],
        targets: [
            .target(name: "MyApplication", dependencies: [
                .product(name: "ServiceLifecycle", package: "swift-service-lifecycle")
            ]),
            .testTarget(name: "MyApplicationTests", dependencies: [
                .target(name: "MyApplication"),
            ]),
        ]
    )
  6. How to use ServiceGroup to orchestrate services

    main

    The ServiceGroup actor is used to orchestrate multiple Service instances in an application. It runs each service in a separate child task and manages their lifecycle.

    Dependency Management: ServiceGroup infers dependencies based on the order of the services array passed to the initializer. Services with a higher index can depend on services with a lower index. To ensure a dependency is available, place the dependency service earlier in the array.

    To use it, initialize ServiceGroup with your services and a Logger, then call run() within an asynchronous context (like @main).

    import ServiceLifecycle
    import Logging
    
    @main
    struct Application {
      static let logger = Logger(label: "Application")
    
      static func main() async throws {
        let fooService = FooService()
        let barService = BarService(fooService: fooService)
    
        let serviceGroup = ServiceGroup(
          // We encode the dependency hierarchy by putting fooService first
          services: [fooService, barService],
          logger: logger
        )
    
        try await serviceGroup.run()
      }
    }
  7. Create a basic ServiceGroupConfiguration

    main

    You can create a ServiceGroupConfiguration by providing a list of services and an optional logger. This configuration defines how a group of services will be managed together.

    Use the following initializers:

    • init(services:logger:): Creates a configuration with the specified services and logger.
    • init(gracefulShutdownSignals:): Creates a configuration with specified services and custom signals for graceful shutdown.
    // Example of creating a basic configuration
    let configuration = ServiceGroupConfiguration(services: [service1, service2], logger: myLogger)
  8. Implement graceful shutdown in services

    main

    Graceful shutdown is a non-forceful alternative to task cancellation. It allows services to clean up resources or finish ongoing work before exiting.

    To enable graceful shutdown at the application level, configure ServiceGroup with gracefulShutdownSignals (e.g., [.sigterm]).

    To make your service logic responsive to a graceful shutdown, use the cancelOnGracefulShutdown() method on any AsyncSequence. This ensures that loops iterating over streams or requests terminate when the shutdown signal is received.

    Note: ServiceGroup shuts down services in reverse startup order, waiting for each service's run() method to return before proceeding to the next.

    import ServiceLifecycle
    import Logging
    
    struct StreamingService: Service {
      // ... implementation ...
    
      func run() async throws {
        await withDiscardingTaskGroup { group in
          // Use cancelOnGracefulShutdown() to stop iteration during shutdown
          for stream in makeStreams().cancelOnGracefulShutdown() {
            group.addTask {
              await streamHandler(stream.requestStream, stream.responseWriter)
            }
          }
        }
      }
    }
    
    @main
    struct Application {
      static let logger = Logger(label: "Application")
    
      static func main() async throws {
        let streamingService = StreamingService(streamHandler: { requestStream, responseWriter in
          // Use cancelOnGracefulShutdown() inside handlers as well
          for await request in requestStream.cancelOnGracefulShutdown() {
            responseWriter.write("response")
          }
        })
    
        let serviceGroup = ServiceGroup(
          services: [streamingService],
          gracefulShutdownSignals: [.sigterm],
          logger: logger
        )
    
        try await serviceGroup.run()
      }
    }
  9. Create a ServiceGroupConfiguration with signal handlers

    main

    For advanced lifecycle management, you can initialize a ServiceGroupConfiguration with specific signals for both graceful shutdown and cancellation. This allows you to define exactly which OS signals trigger different stages of the service teardown process.

    Use the following initializer:

    • init(services:gracefulShutdownSignals:cancellationSignals:logger:): Configures the group with specific services, signals for graceful shutdown, signals for cancellation, and a logger.
    // Example of creating a configuration with custom signals
    let configuration = ServiceGroupConfiguration(
        services: [service1, service2],
        gracefulShutdownSignals: [.sigterm], 
        cancellationSignals: [.sigint], 
        logger: myLogger
    )
  10. Add Swift Service Lifecycle as a dependency

    main

    To use Swift Service Lifecycle in your Swift project, add the package to your Package.swift file. Declare the package dependency using the GitHub URL and then add the ServiceLifecycle product to your specific application target.

    // swift-tools-version:6.0
    import PackageDescription
    
    let package = Package(
        name: "my-application",
        dependencies: [
            .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0"),
        ],
        targets: [
            .target(name: "MyApplication", dependencies: [
                .product(name: "ServiceLifecycle", package: "swift-service-lifecycle")
            ]),
            .testTarget(name: "MyApplicationTests", dependencies: [
                .target(name: "MyApplication"),
            ]),
        ]
    )
  11. Adopting the Service protocol in your library

    main

    To allow your library's components to be coordinated by a ServiceGroup, conform your types to the Service protocol. The protocol requires a single run() method that contains the service's long-running work.

    Best Practices

    1. Use Structured Concurrency: Instead of spawning unstructured Task { ... } blocks in an init(), implement the work inside run(). This ensures that when the service is cancelled, the work is automatically cancelled via Swift's structured concurrency propagation.
    2. Handle Termination: Returning from run() or throwing an error is interpreted as a failure. By default, this causes a ServiceGroup to cancel all other services in the group.
    3. Implement Cancellation: Ensure your run() method reacts to cancellation. If you consume an AsyncSequence or call other cancellation-aware async methods, this is often handled automatically.
    public actor TCPEchoClient: Service {
      public init() { }
    
      public func run() async throws {
        for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) {
          self.sendKeepAlivePings()
        }
      }
    
      private func sendKeepAlivePings() async { ... }
    }
  12. Create a cancelling sequence with AsyncCancelOnGracefulShutdownSequence

    main

    Use AsyncCancelOnGracefulShutdownSequence to wrap an existing sequence of tasks or elements. This allows you to ensure that when a graceful shutdown occurs, the elements in the sequence are cancelled. You initialize it by passing a base sequence to init(base:).

    // Example initialization
    let cancellingSequence = AsyncCancelOnGracefulShutdownSequence(base: someBaseSequence)