├── .vscode └── settings.json ├── .spi.yml ├── CODE_OF_CONDUCT.md ├── .gitignore ├── Sources ├── ServiceLifecycle │ ├── Docs.docc │ │ ├── curation │ │ │ ├── Service.md │ │ │ ├── LoggingConfiguration.md │ │ │ ├── ServiceGroupError-Code.md │ │ │ ├── AsyncCancelOnGracefulShutdownSequence.md │ │ │ ├── ServiceGroupError.md │ │ │ ├── ServiceConfiguration.md │ │ │ ├── ServiceGroup.md │ │ │ └── ServiceGroupConfiguration.md │ │ ├── index.md │ │ ├── Adopting ServiceLifecycle in libraries.md │ │ └── Adopting ServiceLifecycle in applications.md │ ├── AsyncGracefulShutdownSequence.swift │ ├── Service.swift │ ├── CancellationWaiter.swift │ ├── ServiceRunnerError.swift │ ├── AsyncCancelOnGracefulShutdownSequence.swift │ ├── ServiceGroupConfiguration.swift │ └── GracefulShutdown.swift ├── ConcurrencyHelpers │ ├── LockedValueBox.swift │ └── Lock.swift ├── ServiceLifecycleTestKit │ └── GracefulShutdown.swift └── UnixSignals │ ├── UnixSignal.swift │ └── UnixSignalsSequence.swift ├── .editorconfig ├── .mailmap ├── dev └── git.commit.template ├── .github ├── release.yml └── workflows │ ├── pull_request_label.yml │ ├── main.yml │ └── pull_request.yml ├── .licenseignore ├── CONTRIBUTORS.txt ├── NOTICE.txt ├── Tests ├── ServiceLifecycleTests │ ├── XCTest+Async.swift │ ├── MockService.swift │ ├── AsyncCancelOnGracefulShutdownSequenceTests.swift │ ├── ServiceGroupAddServiceTests.swift │ └── GracefulShutdownTests.swift └── UnixSignalsTests │ └── UnixSignalTests.swift ├── SECURITY.md ├── .swift-format ├── Package.swift ├── CONTRIBUTING.md ├── README.md └── LICENSE.txt /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [ServiceLifecycle, UnixSignals] 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm 7 | Package.resolved 8 | 9 | .swift-version 10 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/curation/Service.md: -------------------------------------------------------------------------------- 1 | # ``ServiceLifecycle/Service`` 2 | 3 | ## Topics 4 | 5 | ### Running a service 6 | 7 | - ``run()`` 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | 11 | [*.yml] 12 | indent_style = space 13 | indent_size = 2 -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Tomer Doron 2 | Tomer Doron 3 | Tomer Doron 4 | Konrad `ktoso` Malawski 5 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/curation/LoggingConfiguration.md: -------------------------------------------------------------------------------- 1 | # ``ServiceLifecycle/ServiceGroupConfiguration/LoggingConfiguration`` 2 | 3 | ## Topics 4 | 5 | ### Creating a logging configuration 6 | 7 | - ``init()`` 8 | 9 | ### Inspecting a service group configuration 10 | 11 | - ``keys`` 12 | - ``Keys-swift.struct`` 13 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError-Code.md: -------------------------------------------------------------------------------- 1 | # ``ServiceLifecycle/ServiceGroupError/Code`` 2 | 3 | ## Topics 4 | 5 | ### Service Group Errors 6 | 7 | - ``serviceFinishedUnexpectedly`` 8 | - ``alreadyRunning`` 9 | - ``alreadyFinished`` 10 | 11 | ### Inspecting an error 12 | 13 | - ``description`` 14 | -------------------------------------------------------------------------------- /dev/git.commit.template: -------------------------------------------------------------------------------- 1 | One line description of your change 2 | 3 | Motivation: 4 | 5 | Explain here the context, and why you're making that change. 6 | What is the problem you're trying to solve. 7 | 8 | Modifications: 9 | 10 | Describe the modifications you've done. 11 | 12 | Result: 13 | 14 | After your change, what will change. 15 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/curation/AsyncCancelOnGracefulShutdownSequence.md: -------------------------------------------------------------------------------- 1 | # ``ServiceLifecycle/AsyncCancelOnGracefulShutdownSequence`` 2 | 3 | ## Topics 4 | 5 | ### Creating a cancelling sequence 6 | 7 | - ``init(base:)`` 8 | 9 | ### Iterating the sequence 10 | 11 | - ``Element`` 12 | - ``makeAsyncIterator()`` 13 | - ``AsyncIterator`` 14 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: SemVer Major 4 | labels: 5 | - ⚠️ semver/major 6 | - title: SemVer Minor 7 | labels: 8 | - 🆕 semver/minor 9 | - title: SemVer Patch 10 | labels: 11 | - 🔨 semver/patch 12 | - title: Other Changes 13 | labels: 14 | - semver/none 15 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupError.md: -------------------------------------------------------------------------------- 1 | # ``ServiceLifecycle/ServiceGroupError`` 2 | 3 | ## Topics 4 | 5 | ### Inspecting the errors 6 | 7 | - ``errorCode`` 8 | - ``Code`` 9 | 10 | ### Service Group Errors 11 | 12 | - ``serviceFinishedUnexpectedly(file:line:service:)`` 13 | - ``alreadyRunning(file:line:)`` 14 | - ``alreadyFinished(file:line:)`` 15 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/curation/ServiceConfiguration.md: -------------------------------------------------------------------------------- 1 | # ``ServiceLifecycle/ServiceGroupConfiguration/ServiceConfiguration`` 2 | 3 | ## Topics 4 | 5 | ### Creating a service configuration 6 | 7 | - ``init(service:successTerminationBehavior:failureTerminationBehavior:)`` 8 | 9 | ### Inspecting a service configuration 10 | 11 | - ``service`` 12 | - ``failureTerminationBehavior`` 13 | - ``successTerminationBehavior`` 14 | - ``TerminationBehavior`` 15 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroup.md: -------------------------------------------------------------------------------- 1 | # ``ServiceLifecycle/ServiceGroup`` 2 | 3 | ## Topics 4 | 5 | ### Creating a service group 6 | 7 | - ``init(configuration:)`` 8 | - ``init(services:gracefulShutdownSignals:cancellationSignals:logger:)`` 9 | - ``init(services:configuration:logger:)`` 10 | 11 | ### Adding to a service group 12 | 13 | - ``addServiceUnlessShutdown(_:)-r47h`` 14 | - ``addServiceUnlessShutdown(_:)-9jpoj`` 15 | 16 | ### Running a service group 17 | 18 | - ``run(file:line:)`` 19 | - ``run()`` 20 | - ``triggerGracefulShutdown()`` 21 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_label.yml: -------------------------------------------------------------------------------- 1 | name: PR label 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | types: [labeled, unlabeled, opened, reopened, synchronize] 9 | 10 | jobs: 11 | semver-label-check: 12 | name: Semantic version label check 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 1 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | - name: Check for Semantic Version label 21 | uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main 22 | -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | **/.gitignore 3 | .licenseignore 4 | .gitattributes 5 | .mailfilter 6 | .mailmap 7 | .spi.yml 8 | .swift-format 9 | .editorconfig 10 | .github/* 11 | *.md 12 | *.txt 13 | *.yml 14 | *.yaml 15 | *.json 16 | Package.swift 17 | **/Package.swift 18 | Package@-*.swift 19 | **/Package@-*.swift 20 | Package.resolved 21 | **/Package.resolved 22 | Makefile 23 | *.modulemap 24 | **/*.modulemap 25 | **/*.docc/* 26 | *.xcprivacy 27 | **/*.xcprivacy 28 | *.symlink 29 | **/*.symlink 30 | Dockerfile 31 | **/Dockerfile 32 | Snippets/* 33 | dev/git.commit.template 34 | *.crt 35 | **/*.crt 36 | *.pem 37 | **/*.pem 38 | *.der 39 | **/*.der 40 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to SwiftServiceLifecycle. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | - Apple Inc. (all contributors with '@apple.com') 11 | 12 | ### Contributors 13 | 14 | - Johannes Weiss 15 | - Konrad `ktoso` Malawski 16 | - Tomer Doron 17 | - Yim Lee 18 | 19 | **Updating this list** 20 | 21 | Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` 22 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/curation/ServiceGroupConfiguration.md: -------------------------------------------------------------------------------- 1 | # ``ServiceLifecycle/ServiceGroupConfiguration`` 2 | 3 | ## Topics 4 | 5 | ### Creating a service group configuration 6 | 7 | - ``init(services:logger:)-309oa`` 8 | - ``init(services:logger:)-2mhze`` 9 | - ``init(gracefulShutdownSignals:)`` 10 | 11 | ### Creating a new service group configuration with signal handlers 12 | 13 | - ``init(services:gracefulShutdownSignals:cancellationSignals:logger:)-9uhzu`` 14 | - ``init(services:gracefulShutdownSignals:cancellationSignals:logger:)-1noxs`` 15 | 16 | ### Inspecting the service group services 17 | 18 | - ``services`` 19 | - ``ServiceConfiguration`` 20 | 21 | ### Inspecting the service group logging 22 | 23 | - ``logging`` 24 | - ``LoggingConfiguration`` 25 | - ``logger`` 26 | 27 | ### Inspecting the service group signal handling 28 | 29 | - ``cancellationSignals`` 30 | - ``maximumCancellationDuration`` 31 | - ``gracefulShutdownSignals`` 32 | - ``maximumGracefulShutdownDuration`` 33 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | The ServiceLifecycle Project 3 | =========================== 4 | 5 | Please visit the ServiceLifecycle web site for more information: 6 | 7 | * https://github.com/swift-server/swift-service-lifecycle 8 | 9 | Copyright 2019-2023 The ServiceLifecycle Project 10 | 11 | The ServiceLifecycle Project licenses this file to you under the Apache License, 12 | version 2.0 (the "License"); you may not use this file except in compliance 13 | with the License. You may obtain a copy of the License at: 14 | 15 | https://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 19 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 20 | License for the specific language governing permissions and limitations 21 | under the License. 22 | 23 | Also, please refer to each LICENSE.txt file, which is located in 24 | the 'license' directory of the distribution file, for the license terms of the 25 | components that this product depends on. 26 | 27 | --- 28 | 29 | This product contains derivations of the Lock and LockedValueBox implementations from SwiftNIO. 30 | 31 | * LICENSE (Apache License 2.0): 32 | * https://www.apache.org/licenses/LICENSE-2.0 33 | * HOMEPAGE: 34 | * https://github.com/apple/swift-nio 35 | 36 | --- 37 | 38 | This product uses swift-async-algorithms. 39 | 40 | * LICENSE (Apache License 2.0): 41 | * https://www.apache.org/licenses/LICENSE-2.0 42 | * HOMEPAGE: 43 | * https://github.com/apple/swift-async-algorithms 44 | 45 | --- 46 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/AsyncGracefulShutdownSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// An async sequence that emits an element once graceful shutdown has been triggered. 16 | /// 17 | /// This sequence is a broadcast async sequence and only produces one value and then finishes. 18 | /// 19 | /// - Note: This sequence respects cancellation and thus is `throwing`. 20 | @usableFromInline 21 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 22 | struct AsyncGracefulShutdownSequence: AsyncSequence, Sendable { 23 | @usableFromInline 24 | typealias Element = CancellationWaiter.Reason 25 | 26 | @inlinable 27 | init() {} 28 | 29 | @inlinable 30 | func makeAsyncIterator() -> AsyncIterator { 31 | AsyncIterator() 32 | } 33 | 34 | @usableFromInline 35 | struct AsyncIterator: AsyncIteratorProtocol { 36 | @inlinable 37 | init() {} 38 | 39 | @inlinable 40 | func next() async -> Element? { 41 | await CancellationWaiter().wait() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | schedule: 10 | - cron: "0 8,20 * * *" 11 | 12 | jobs: 13 | unit-tests: 14 | name: Unit tests 15 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 16 | with: 17 | linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 18 | linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 19 | linux_6_2_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 20 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 21 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 22 | windows_6_0_enabled: true 23 | windows_6_1_enabled: true 24 | windows_6_2_enabled: true 25 | windows_nightly_next_enabled: true 26 | windows_nightly_main_enabled: true 27 | 28 | cxx-interop: 29 | name: Cxx interop 30 | uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main 31 | 32 | static-sdk: 33 | name: Static SDK 34 | # Workaround https://github.com/nektos/act/issues/1875 35 | uses: apple/swift-nio/.github/workflows/static_sdk.yml@main 36 | 37 | release-builds: 38 | name: Release builds 39 | uses: apple/swift-nio/.github/workflows/release_builds.yml@main 40 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | soundness: 12 | name: Soundness 13 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 14 | with: 15 | license_header_check_project_name: "SwiftServiceLifecycle" 16 | 17 | unit-tests: 18 | name: Unit tests 19 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 20 | with: 21 | linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 22 | linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 23 | linux_6_2_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 24 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 25 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 26 | windows_6_0_enabled: true 27 | windows_6_1_enabled: true 28 | windows_6_2_enabled: true 29 | windows_nightly_next_enabled: true 30 | windows_nightly_main_enabled: true 31 | 32 | cxx-interop: 33 | name: Cxx interop 34 | uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main 35 | 36 | static-sdk: 37 | name: Static SDK 38 | # Workaround https://github.com/nektos/act/issues/1875 39 | uses: apple/swift-nio/.github/workflows/static_sdk.yml@main 40 | 41 | release-builds: 42 | name: Release builds 43 | uses: apple/swift-nio/.github/workflows/release_builds.yml@main 44 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Service.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// A type that represents a long-running task. 16 | /// 17 | /// This is the protocol that a service implements. 18 | /// 19 | /// ``ServiceGroup`` calls the asynchronous ``Service/run()`` method when it starts a service. 20 | /// A service group expects the `run` method to stay running until the process is terminated. 21 | /// When implementing a service, return or throw an error from `run` to indicate the service should stop. 22 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 23 | public protocol Service: Sendable { 24 | /// The service group calls this method when it starts the service. 25 | /// 26 | /// Your implementation should execute its long running work in this method such as: 27 | /// - Handling incoming connections and requests 28 | /// - Background refreshes 29 | /// 30 | /// - Important: Returning or throwing from this method indicates the service should stop and causes the 31 | /// ``ServiceGroup`` to follow behaviors for the child tasks of all other running services specified in 32 | /// ``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` and 33 | /// ``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``. 34 | func run() async throws 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/CancellationWaiter.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// An actor that provides a function to wait on cancellation/graceful shutdown. 16 | @usableFromInline 17 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 18 | actor CancellationWaiter { 19 | @usableFromInline 20 | enum Reason: Sendable { 21 | case cancelled 22 | case gracefulShutdown 23 | } 24 | 25 | private var taskContinuation: CheckedContinuation? 26 | 27 | @usableFromInline 28 | init() {} 29 | 30 | @usableFromInline 31 | func wait() async -> Reason { 32 | await withTaskCancellationHandler { 33 | await withGracefulShutdownHandler { 34 | await withCheckedContinuation { (continuation: CheckedContinuation) in 35 | self.taskContinuation = continuation 36 | } 37 | } onGracefulShutdown: { 38 | Task { 39 | await self.finish(reason: .gracefulShutdown) 40 | } 41 | } 42 | } onCancel: { 43 | Task { 44 | await self.finish(reason: .cancelled) 45 | } 46 | } 47 | } 48 | 49 | private func finish(reason: Reason) { 50 | self.taskContinuation?.resume(returning: reason) 51 | self.taskContinuation = nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/ServiceLifecycleTests/XCTest+Async.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | 17 | func XCTAsyncAssertEqual( 18 | _ expression1: @autoclosure () async throws -> T, 19 | _ expression2: @autoclosure () async throws -> T, 20 | _ message: @autoclosure () -> String = "", 21 | file: StaticString = #filePath, 22 | line: UInt = #line 23 | ) async rethrows where T: Equatable { 24 | let result1 = try await expression1() 25 | let result2 = try await expression2() 26 | XCTAssertEqual(result1, result2, message(), file: file, line: line) 27 | } 28 | 29 | func XCTAsyncAssertThrowsError( 30 | _ expression: @autoclosure () async throws -> some Any, 31 | _ message: @autoclosure () -> String = "", 32 | file: StaticString = #filePath, 33 | line: UInt = #line, 34 | _ errorHandler: (_ error: Error) -> Void = { _ in } 35 | ) async { 36 | do { 37 | _ = try await expression() 38 | XCTFail(message(), file: file, line: line) 39 | } catch { 40 | errorHandler(error) 41 | } 42 | } 43 | 44 | func XCTAsyncAssertNoThrow( 45 | _ expression: @autoclosure () async throws -> some Any, 46 | _ message: @autoclosure () -> String = "", 47 | file: StaticString = #filePath, 48 | line: UInt = #line 49 | ) async { 50 | do { 51 | _ = try await expression() 52 | } catch { 53 | XCTFail("Threw error \(error)", file: file, line: line) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | This document specifies the security process for the Swift Service Lifecycle project. 4 | 5 | ## Disclosures 6 | 7 | ### Private Disclosure Process 8 | 9 | The Swift Service Lifecycle maintainers ask that known and suspected vulnerabilities be 10 | privately and responsibly disclosed by emailing 11 | [sswg-security-reports@forums.swift.org](mailto:sswg-security-reports@forums.swift.org) 12 | with the all the required detail. 13 | **Do not file a public issue.** 14 | 15 | #### When to report a vulnerability 16 | 17 | * You think you have discovered a potential security vulnerability in Swift Service Lifecycle. 18 | * You are unsure how a vulnerability affects Swift Service Lifecycle. 19 | 20 | #### What happens next? 21 | 22 | * A member of the team will acknowledge receipt of the report within 3 23 | working days (United States). This may include a request for additional 24 | information about reproducing the vulnerability. 25 | * We will privately inform the Swift Server Work Group ([SSWG][sswg]) of the 26 | vulnerability within 10 days of the report as per their [security 27 | guidelines][sswg-security]. 28 | * Once we have identified a fix we may ask you to validate it. We aim to do this 29 | within 30 days. In some cases this may not be possible, for example when the 30 | vulnerability exists at the protocol level and the industry must coordinate on 31 | the disclosure process. 32 | * If a CVE number is required, one will be requested from [MITRE][mitre] 33 | providing you with full credit for the discovery. 34 | * We will decide on a planned release date and let you know when it is. 35 | * Prior to release, we will inform major dependents that a security-related 36 | patch is impending. 37 | * Once the fix has been released we will publish a security advisory on GitHub 38 | and in the Server → Security Updates category on the [Swift forums][swift-forums-sec]. 39 | 40 | [sswg]: https://github.com/swift-server/sswg 41 | [sswg-security]: https://github.com/swift-server/sswg/blob/main/security/README.md 42 | [swift-forums-sec]: https://forums.swift.org/c/server/security-updates/ 43 | [mitre]: https://cveform.mitre.org/ 44 | -------------------------------------------------------------------------------- /Sources/ConcurrencyHelpers/LockedValueBox.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | //===----------------------------------------------------------------------===// 15 | // 16 | // This source file is part of the SwiftNIO open source project 17 | // 18 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 19 | // Licensed under Apache License v2.0 20 | // 21 | // See LICENSE.txt for license information 22 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 23 | // 24 | // SPDX-License-Identifier: Apache-2.0 25 | // 26 | //===----------------------------------------------------------------------===// 27 | 28 | /// Provides locked access to `Value`. 29 | /// 30 | /// - note: ``LockedValueBox`` has reference semantics and holds the `Value` 31 | /// alongside a lock behind a reference. 32 | /// 33 | /// This is no different than creating a ``Lock`` and protecting all 34 | /// accesses to a value using the lock. But it's easy to forget to actually 35 | /// acquire/release the lock in the correct place. ``LockedValueBox`` makes 36 | /// that much easier. 37 | public struct LockedValueBox { 38 | @usableFromInline 39 | internal let _storage: LockStorage 40 | 41 | /// Initialize the `Value`. 42 | @inlinable 43 | public init(_ value: Value) { 44 | self._storage = .create(value: value) 45 | } 46 | 47 | /// Access the `Value`, allowing mutation of it. 48 | @inlinable 49 | public func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { 50 | return try self._storage.withLockedValue(mutate) 51 | } 52 | } 53 | 54 | extension LockedValueBox: @unchecked Sendable where Value: Sendable {} 55 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentation" : { 6 | "spaces" : 4 7 | }, 8 | "indentConditionalCompilationBlocks" : false, 9 | "indentSwitchCaseLabels" : false, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : true, 13 | "lineBreakBeforeEachGenericRequirement" : true, 14 | "lineLength" : 120, 15 | "maximumBlankLines" : 1, 16 | "prioritizeKeepingFunctionOutputTogether" : true, 17 | "respectsExistingLineBreaks" : true, 18 | "rules" : { 19 | "AllPublicDeclarationsHaveDocumentation" : false, 20 | "AlwaysUseLowerCamelCase" : false, 21 | "AmbiguousTrailingClosureOverload" : true, 22 | "BeginDocumentationCommentWithOneLineSummary" : false, 23 | "DoNotUseSemicolons" : true, 24 | "DontRepeatTypeInStaticProperties" : true, 25 | "FileScopedDeclarationPrivacy" : true, 26 | "FullyIndirectEnum" : true, 27 | "GroupNumericLiterals" : true, 28 | "IdentifiersMustBeASCII" : true, 29 | "NeverForceUnwrap" : false, 30 | "NeverUseForceTry" : false, 31 | "NeverUseImplicitlyUnwrappedOptionals" : false, 32 | "NoAccessLevelOnExtensionDeclaration" : true, 33 | "NoAssignmentInExpressions" : true, 34 | "NoBlockComments" : true, 35 | "NoCasesWithOnlyFallthrough" : true, 36 | "NoEmptyTrailingClosureParentheses" : true, 37 | "NoLabelsInCasePatterns" : false, 38 | "NoLeadingUnderscores" : false, 39 | "NoParensAroundConditions" : true, 40 | "NoVoidReturnOnFunctionSignature" : true, 41 | "OneCasePerLine" : true, 42 | "OneVariableDeclarationPerLine" : true, 43 | "OnlyOneTrailingClosureArgument" : true, 44 | "OrderedImports" : false, 45 | "ReturnVoidInsteadOfEmptyTuple" : true, 46 | "UseEarlyExits" : true, 47 | "UseLetInEveryBoundCaseVariable" : false, 48 | "UseShorthandTypeNames" : true, 49 | "UseSingleLinePropertyGetter" : false, 50 | "UseSynthesizedInitializer" : false, 51 | "UseTripleSlashForDocumentationComments" : true, 52 | "UseWhereClausesInForLoops" : false, 53 | "ValidateDocumentationComments" : false 54 | }, 55 | "spacesAroundRangeFormationOperators" : false, 56 | "tabWidth" : 4, 57 | "version" : 1 58 | } -------------------------------------------------------------------------------- /Sources/ServiceLifecycleTestKit/GracefulShutdown.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | @_spi(TestKit) import ServiceLifecycle 16 | 17 | /// This struct is used in testing graceful shutdown. 18 | /// 19 | /// It is passed to the `operation` closure of the ``testGracefulShutdown(operation:)`` method and allows 20 | /// to trigger the graceful shutdown for testing purposes. 21 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 22 | public struct GracefulShutdownTestTrigger: Sendable { 23 | private let gracefulShutdownManager: GracefulShutdownManager 24 | 25 | init(gracefulShutdownManager: GracefulShutdownManager) { 26 | self.gracefulShutdownManager = gracefulShutdownManager 27 | } 28 | 29 | /// Triggers the graceful shutdown. 30 | public func triggerGracefulShutdown() { 31 | self.gracefulShutdownManager.shutdownGracefully() 32 | } 33 | } 34 | 35 | /// Use this method for testing your graceful shutdown behaviour. 36 | /// 37 | /// Call the code that you want to test inside the `operation` closure and trigger the graceful shutdown by calling ``GracefulShutdownTestTrigger/triggerGracefulShutdown()`` 38 | /// on the ``GracefulShutdownTestTrigger`` that is passed to the `operation` closure. 39 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 40 | public func testGracefulShutdown(operation: (GracefulShutdownTestTrigger) async throws -> T) async rethrows -> T { 41 | let gracefulShutdownManager = GracefulShutdownManager() 42 | return try await TaskLocals.$gracefulShutdownManager.withValue(gracefulShutdownManager) { 43 | let gracefulShutdownTestTrigger = GracefulShutdownTestTrigger(gracefulShutdownManager: gracefulShutdownManager) 44 | return try await operation(gracefulShutdownTestTrigger) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.8 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "swift-service-lifecycle", 6 | products: [ 7 | .library( 8 | name: "ServiceLifecycle", 9 | targets: ["ServiceLifecycle"] 10 | ), 11 | .library( 12 | name: "ServiceLifecycleTestKit", 13 | targets: ["ServiceLifecycleTestKit"] 14 | ), 15 | .library( 16 | name: "UnixSignals", 17 | targets: ["UnixSignals"] 18 | ), 19 | ], 20 | dependencies: [ 21 | .package( 22 | url: "https://github.com/apple/swift-log.git", 23 | from: "1.5.2" 24 | ), 25 | .package( 26 | url: "https://github.com/apple/swift-async-algorithms.git", 27 | from: "1.0.4" 28 | ), 29 | ], 30 | targets: [ 31 | .target( 32 | name: "ServiceLifecycle", 33 | dependencies: [ 34 | .product( 35 | name: "Logging", 36 | package: "swift-log" 37 | ), 38 | .product( 39 | name: "AsyncAlgorithms", 40 | package: "swift-async-algorithms" 41 | ), 42 | .target(name: "UnixSignals"), 43 | .target(name: "ConcurrencyHelpers"), 44 | ] 45 | ), 46 | .target( 47 | name: "ServiceLifecycleTestKit", 48 | dependencies: [ 49 | .target(name: "ServiceLifecycle") 50 | ] 51 | ), 52 | .target( 53 | name: "UnixSignals", 54 | dependencies: [ 55 | .target(name: "ConcurrencyHelpers") 56 | ] 57 | ), 58 | .target( 59 | name: "ConcurrencyHelpers" 60 | ), 61 | .testTarget( 62 | name: "ServiceLifecycleTests", 63 | dependencies: [ 64 | .target(name: "ServiceLifecycle"), 65 | .target(name: "ServiceLifecycleTestKit"), 66 | ] 67 | ), 68 | .testTarget( 69 | name: "UnixSignalsTests", 70 | dependencies: [ 71 | .target(name: "UnixSignals") 72 | ] 73 | ), 74 | ] 75 | ) 76 | 77 | for target in package.targets { 78 | target.swiftSettings?.append(.enableUpcomingFeature("StrictConcurrency")) 79 | } 80 | -------------------------------------------------------------------------------- /Tests/ServiceLifecycleTests/MockService.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ServiceLifecycle 16 | 17 | actor MockService: Service, CustomStringConvertible { 18 | enum Event { 19 | case run 20 | case runPing 21 | case runCancelled 22 | case shutdownGracefully 23 | } 24 | 25 | let events: AsyncStream 26 | private(set) var hasRun: Bool = false 27 | 28 | private let eventsContinuation: AsyncStream.Continuation 29 | 30 | private var runContinuation: CheckedContinuation? 31 | 32 | nonisolated let description: String 33 | 34 | private let pings: AsyncStream 35 | private nonisolated let pingContinuation: AsyncStream.Continuation 36 | 37 | init( 38 | description: String 39 | ) { 40 | var eventsContinuation: AsyncStream.Continuation! 41 | events = AsyncStream { eventsContinuation = $0 } 42 | self.eventsContinuation = eventsContinuation! 43 | 44 | var pingContinuation: AsyncStream.Continuation! 45 | pings = AsyncStream { pingContinuation = $0 } 46 | self.pingContinuation = pingContinuation! 47 | 48 | self.description = description 49 | } 50 | 51 | func run() async throws { 52 | hasRun = true 53 | 54 | try await withTaskCancellationHandler { 55 | try await withGracefulShutdownHandler { 56 | try await withThrowingTaskGroup(of: Void.self) { group in 57 | group.addTask { 58 | self.eventsContinuation.yield(.run) 59 | for await _ in self.pings { 60 | self.eventsContinuation.yield(.runPing) 61 | } 62 | } 63 | 64 | try await withCheckedThrowingContinuation { 65 | self.runContinuation = $0 66 | } 67 | 68 | group.cancelAll() 69 | } 70 | } onGracefulShutdown: { 71 | self.eventsContinuation.yield(.shutdownGracefully) 72 | } 73 | } onCancel: { 74 | self.eventsContinuation.yield(.runCancelled) 75 | } 76 | } 77 | 78 | func resumeRunContinuation(with result: Result) { 79 | runContinuation?.resume(with: result) 80 | } 81 | 82 | nonisolated func sendPing() { 83 | pingContinuation.yield() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/ServiceLifecycleTests/AsyncCancelOnGracefulShutdownSequenceTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ServiceLifecycle 16 | import ServiceLifecycleTestKit 17 | import XCTest 18 | 19 | final class AsyncCancelOnGracefulShutdownSequenceTests: XCTestCase { 20 | func testCancelOnGracefulShutdown_finishesWhenShutdown() async throws { 21 | await testGracefulShutdown { gracefulShutdownTrigger in 22 | let stream = AsyncStream { 23 | try? await Task.sleep(nanoseconds: 100_000_000) 24 | return 1 25 | } 26 | 27 | let (resultStream, resultContinuation) = AsyncStream.makeStream() 28 | 29 | await withTaskGroup(of: Void.self) { group in 30 | group.addTask { 31 | for await value in stream.cancelOnGracefulShutdown() { 32 | resultContinuation.yield(value) 33 | } 34 | resultContinuation.finish() 35 | } 36 | 37 | var iterator = resultStream.makeAsyncIterator() 38 | 39 | await XCTAsyncAssertEqual(await iterator.next(), 1) 40 | 41 | gracefulShutdownTrigger.triggerGracefulShutdown() 42 | 43 | await XCTAsyncAssertEqual(await iterator.next(), nil) 44 | } 45 | } 46 | } 47 | 48 | func testCancelOnGracefulShutdown_finishesBaseFinishes() async throws { 49 | await testGracefulShutdown { _ in 50 | let (baseStream, baseContinuation) = AsyncStream.makeStream() 51 | let (resultStream, resultContinuation) = AsyncStream.makeStream() 52 | 53 | await withTaskGroup(of: Void.self) { group in 54 | group.addTask { 55 | for await value in baseStream.cancelOnGracefulShutdown() { 56 | resultContinuation.yield(value) 57 | } 58 | resultContinuation.finish() 59 | } 60 | 61 | var iterator = resultStream.makeAsyncIterator() 62 | baseContinuation.yield(1) 63 | 64 | await XCTAsyncAssertEqual(await iterator.next(), 1) 65 | 66 | baseContinuation.finish() 67 | 68 | await XCTAsyncAssertEqual(await iterator.next(), nil) 69 | } 70 | } 71 | } 72 | } 73 | 74 | extension AsyncStream { 75 | fileprivate static func makeStream( 76 | of elementType: Element.Type = Element.self, 77 | bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded 78 | ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { 79 | var continuation: AsyncStream.Continuation! 80 | let stream = AsyncStream(bufferingPolicy: limit) { continuation = $0 } 81 | return (stream: stream, continuation: continuation!) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``ServiceLifecycle`` 2 | 3 | A library for cleanly starting up and shutting down applications. 4 | 5 | ## Overview 6 | 7 | Applications often have to orchestrate multiple internal services that orchestrate their business 8 | logic, such as clients or servers. This quickly becomes tedious; especially when the APIs of 9 | different services do not interoperate nicely with one another. This library sets out to solve the 10 | issue by providing a ``Service`` protocol for services to implement, and an orchestrator, the 11 | ``ServiceGroup``, to handle running of various services. 12 | 13 | This library is fully based on Swift Structured Concurrency, allowing it to safely orchestrate the 14 | individual services in separate child tasks. Furthermore, the library complements the cooperative 15 | task cancellation from Structured Concurrency with a new mechanism called _graceful shutdown_. While 16 | cancellation is a signal for a task to stop its work as soon as possible, _graceful shutdown_ 17 | indicates a task that it should eventually shutdown, but it is up to the underlying business logic 18 | to decide if and when that happens. 19 | 20 | ``ServiceLifecycle`` should be used by both library and application authors to create a seamless experience. 21 | Library authors should conform their services to the ``Service`` protocol and application authors 22 | should use the ``ServiceGroup`` to orchestrate all their services. 23 | 24 | ## Getting started 25 | 26 | 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. 27 | Below, you will find information to get started. 28 | 29 | ### Adding the dependency 30 | 31 | To add a dependency on the package, declare it in your `Package.swift`: 32 | 33 | ```swift 34 | .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0"), 35 | ``` 36 | 37 | and add `ServiceLifecycle` to the dependencies of your application target: 38 | 39 | ```swift 40 | .product(name: "ServiceLifecycle", package: "swift-service-lifecycle") 41 | ``` 42 | 43 | The following example `Package.swift` illustrates a package with `ServiceLifecycle` as a dependency: 44 | 45 | ```swift 46 | // swift-tools-version:6.0 47 | import PackageDescription 48 | 49 | let package = Package( 50 | name: "my-application", 51 | dependencies: [ 52 | .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0"), 53 | ], 54 | targets: [ 55 | .target(name: "MyApplication", dependencies: [ 56 | .product(name: "ServiceLifecycle", package: "swift-service-lifecycle") 57 | ]), 58 | .testTarget(name: "MyApplicationTests", dependencies: [ 59 | .target(name: "MyApplication"), 60 | ]), 61 | ] 62 | ) 63 | ``` 64 | 65 | ## Topics 66 | 67 | ### Articles 68 | 69 | - 70 | - 71 | 72 | ### Service protocol 73 | 74 | - ``Service`` 75 | 76 | ### Service Group 77 | 78 | - ``ServiceGroup`` 79 | - ``ServiceGroupConfiguration`` 80 | - ``ServiceGroupError`` 81 | 82 | ### Graceful Shutdown 83 | 84 | - ``gracefulShutdown()`` 85 | - ``cancelWhenGracefulShutdown(_:)`` 86 | - ``withTaskCancellationOrGracefulShutdownHandler(isolation:operation:onCancelOrGracefulShutdown:)`` 87 | - ``withGracefulShutdownHandler(isolation:operation:onGracefulShutdown:)`` 88 | - ``AsyncCancelOnGracefulShutdownSequence`` 89 | 90 | - ``cancelOnGracefulShutdown(_:)`` 91 | - ``withGracefulShutdownHandler(operation:onGracefulShutdown:)`` 92 | - ``withGracefulShutdownHandler(operation:onGracefulShutdown:)-1x21p`` 93 | - ``withTaskCancellationOrGracefulShutdownHandler(operation:onCancelOrGracefulShutdown:)-81m01`` 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to Apple and the community, and agree by submitting the patch 5 | that your contributions are licensed under the Apache 2.0 license (see 6 | `LICENSE.txt`). 7 | 8 | 9 | ## How to submit a bug report 10 | 11 | Please ensure to specify the following: 12 | 13 | * SwiftServiceLifecycle commit hash 14 | * Contextual information (e.g. what you were trying to achieve with SwiftServiceLifecycle) 15 | * Simplest possible steps to reproduce 16 | * More complex the steps are, lower the priority will be. 17 | * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. 18 | * Anything that might be relevant in your opinion, such as: 19 | * Swift version or the output of `swift --version` 20 | * OS version and the output of `uname -a` 21 | * Network configuration 22 | 23 | 24 | ### Example 25 | 26 | ``` 27 | SwiftServiceLifecycle commit hash: 22ec043dc9d24bb011b47ece4f9ee97ee5be2757 28 | 29 | Context: 30 | While load testing my HTTP web server written with SwiftServiceLifecycle, I noticed 31 | that one file descriptor is leaked per request. 32 | 33 | Steps to reproduce: 34 | 1. ... 35 | 2. ... 36 | 3. ... 37 | 4. ... 38 | 39 | $ swift --version 40 | Swift version 4.0.2 (swift-4.0.2-RELEASE) 41 | Target: x86_64-unknown-linux-gnu 42 | 43 | Operating system: Ubuntu Linux 16.04 64-bit 44 | 45 | $ uname -a 46 | Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 47 | 48 | My system has IPv6 disabled. 49 | ``` 50 | 51 | ## Writing a Patch 52 | 53 | A good SwiftServiceLifecycle patch is: 54 | 55 | 1. Concise, and contains as few changes as needed to achieve the end result. 56 | 2. Tested, ensuring that any tests provided failed before the patch and pass after it. 57 | 3. Documented, adding API documentation as needed to cover new functions and properties. 58 | 4. Accompanied by a great commit message, using our commit message template. 59 | 60 | ### Commit Message Template 61 | 62 | We require that your commit messages match our template. The easiest way to do that is to get git to help you by explicitly using the template. To do that, `cd` to the root of our repository and run: 63 | 64 | git config commit.template dev/git.commit.template 65 | 66 | ### Make sure Tests work on Linux 67 | 68 | SwiftServiceLifecycle uses XCTest to run tests on both macOS and Linux. While the macOS version of XCTest is able to use the Objective-C runtime to discover tests at execution time, the Linux version is not. 69 | For this reason, whenever you add new tests **you have to run a script** that generates the hooks needed to run those tests on Linux, or our CI will complain that the tests are not all present on Linux. To do this, merely execute `ruby ./scripts/generate_linux_tests.rb` at the root of the package and check the changes it made. 70 | 71 | ### Run `./scripts/soundness.sh` 72 | 73 | The scripts directory contains a [soundness.sh script](scripts/soundness.sh) 74 | that enforces additional checks, like license headers and formatting style. 75 | Please make sure to `./scripts/soundness.sh` before pushing a change upstream, otherwise it is likely the PR validation will fail 76 | on minor changes such as a missing `self.` or similar formatting issues. 77 | 78 | > The script also executes the above mentioned `generate_linux_tests.rb`. 79 | 80 | For frequent contributors, we recommend adding the script as a [git pre-push hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), which you can do via executing the following command 81 | in the project root directory: 82 | 83 | ```bash 84 | cat << EOF > .git/hooks/pre-push 85 | #!/bin/bash 86 | 87 | if [[ -f "scripts/soundness.sh" ]]; then 88 | scripts/soundness.sh 89 | fi 90 | EOF 91 | ``` 92 | Which makes the script execute, and only allow the `git push` to complete if the check has passed. 93 | 94 | In the case of formatting issues, you can then `git add` the formatting changes, and attempt the push again. 95 | 96 | ## How to contribute your work 97 | 98 | Please open a pull request at https://github.com/swift-server/swift-service-lifecycle. Make sure the CI passes, and then wait for code review. 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Service Lifecycle 2 | 3 | Swift Service Lifecycle provides a basic mechanism to cleanly start up and shut down an application, freeing resources in-order before exiting. 4 | It also provides a `Signal`-based shutdown hook, to shut down on signals like `TERM` or `INT`. 5 | 6 | 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. 7 | 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. 8 | 9 | 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. 10 | 11 | ## Getting started 12 | 13 | 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. 14 | 15 | ### Adding the dependency 16 | 17 | To add a dependency on the package, declare it in your `Package.swift`: 18 | 19 | ```swift 20 | .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"), 21 | ``` 22 | 23 | and add `ServiceLifecycle` to the dependencies of your application target: 24 | 25 | ```swift 26 | .product(name: "ServiceLifecycle", package: "swift-service-lifecycle") 27 | ``` 28 | 29 | Example `Package.swift` file with `ServiceLifecycle` as a dependency: 30 | 31 | ```swift 32 | // swift-tools-version:6.0 33 | import PackageDescription 34 | 35 | let package = Package( 36 | name: "my-application", 37 | dependencies: [ 38 | .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0"), 39 | ], 40 | targets: [ 41 | .target(name: "MyApplication", dependencies: [ 42 | .product(name: "ServiceLifecycle", package: "swift-service-lifecycle") 43 | ]), 44 | .testTarget(name: "MyApplicationTests", dependencies: [ 45 | .target(name: "MyApplication"), 46 | ]), 47 | ] 48 | ) 49 | ``` 50 | 51 | ### Using ServiceLifecycle 52 | 53 | You can find a short usage example below. You can find more detailed 54 | documentation on how to use ServiceLifecycle 55 | [here](https://swiftpackageindex.com/swift-server/swift-service-lifecycle/main/documentation/servicelifecycle). 56 | 57 | ServiceLifecycle consists of two main building blocks. The `Service` protocol and the `ServiceGroup` 58 | actor. As a library or application developer you should model your long-running work as services 59 | that implement the `Service` protocol. The protocol only requires the implementation of a single 60 | `func run() async throws` method. Once implemented, your application you can use the `ServiceGroup` 61 | to orchestrate multiple services. The group will spawn a child task for each service and call the 62 | respective `run` method in the child task. Furthermore, the group will setup signal listeners for 63 | the configured signals and trigger a graceful shutdown on each service. 64 | 65 | ```swift 66 | import ServiceLifecycle 67 | import Logging 68 | 69 | // A service can be implemented by a struct, class or actor. For this example we are using a struct. 70 | struct FooService: Service { 71 | func run() async throws { 72 | print("FooService starting") 73 | try await Task.sleep(for: .seconds(10)) 74 | print("FooService done") 75 | } 76 | } 77 | 78 | @main 79 | struct Application { 80 | static let logger = Logger(label: "Application") 81 | 82 | static func main() async throws { 83 | let service1 = FooService() 84 | let service2 = FooService() 85 | 86 | let serviceGroup = ServiceGroup( 87 | services: [service1, service2], 88 | gracefulShutdownSignals: [.sigterm], 89 | logger: logger 90 | ) 91 | 92 | try await serviceGroup.run() 93 | } 94 | } 95 | ``` 96 | 97 | ## Security 98 | 99 | Please see [SECURITY.md](SECURITY.md) for details on the security process. 100 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/ServiceRunnerError.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// Errors thrown by a service group. 16 | public struct ServiceGroupError: Error, Hashable, Sendable { 17 | /// A struct that represents the possible error codes. 18 | public struct Code: Hashable, Sendable, CustomStringConvertible { 19 | private enum _Code: Hashable, Sendable { 20 | case alreadyRunning 21 | case alreadyFinished 22 | case serviceFinishedUnexpectedly 23 | } 24 | 25 | private var code: _Code 26 | 27 | private init(code: _Code) { 28 | self.code = code 29 | } 30 | 31 | /// A string representation of a service group error. 32 | public var description: String { 33 | switch self.code { 34 | case .alreadyRunning: 35 | return "The service group is already running the services." 36 | case .alreadyFinished: 37 | return "The service group has already finished running the services." 38 | case .serviceFinishedUnexpectedly: 39 | return "A service has finished unexpectedly." 40 | } 41 | } 42 | 43 | /// Indicates that the service group is already running. 44 | public static let alreadyRunning = Code(code: .alreadyRunning) 45 | /// Indicates that the service group has already finished running. 46 | public static let alreadyFinished = Code(code: .alreadyFinished) 47 | /// Indicates that a service finished unexpectedly. 48 | public static let serviceFinishedUnexpectedly = Code(code: .serviceFinishedUnexpectedly) 49 | } 50 | 51 | /// Internal class that contains the actual error code. 52 | private final class Backing: Hashable, Sendable { 53 | let message: String? 54 | let errorCode: Code 55 | let file: String 56 | let line: Int 57 | 58 | init(errorCode: Code, file: String, line: Int, message: String?) { 59 | self.errorCode = errorCode 60 | self.file = file 61 | self.line = line 62 | self.message = message 63 | } 64 | 65 | static func == (lhs: Backing, rhs: Backing) -> Bool { 66 | lhs.errorCode == rhs.errorCode 67 | } 68 | 69 | func hash(into hasher: inout Hasher) { 70 | hasher.combine(self.errorCode) 71 | } 72 | } 73 | 74 | /// The backing storage of the error. 75 | private let backing: Backing 76 | 77 | /// The error code. 78 | /// 79 | /// - Note: This is the only thing used for the `Equatable` and `Hashable` comparisons for instances of `ServiceGroupError`. 80 | public var errorCode: Code { 81 | self.backing.errorCode 82 | } 83 | 84 | private init(_ backing: Backing) { 85 | self.backing = backing 86 | } 87 | 88 | /// Indicates that the service group is already running. 89 | public static func alreadyRunning(file: String = #fileID, line: Int = #line) -> Self { 90 | Self( 91 | .init( 92 | errorCode: .alreadyRunning, 93 | file: file, 94 | line: line, 95 | message: "" 96 | ) 97 | ) 98 | } 99 | 100 | /// Indicates that the service group has already finished running. 101 | public static func alreadyFinished(file: String = #fileID, line: Int = #line) -> Self { 102 | Self( 103 | .init( 104 | errorCode: .alreadyFinished, 105 | file: file, 106 | line: line, 107 | message: "" 108 | ) 109 | ) 110 | } 111 | 112 | /// Indicates that a service finished unexpectedly even though it indicated it is a long running service. 113 | public static func serviceFinishedUnexpectedly( 114 | file: String = #fileID, 115 | line: Int = #line, 116 | service: String? = nil 117 | ) -> Self { 118 | Self( 119 | .init( 120 | errorCode: .serviceFinishedUnexpectedly, 121 | file: file, 122 | line: line, 123 | message: service.flatMap { "Service failed(\($0))" } 124 | ) 125 | ) 126 | } 127 | } 128 | 129 | extension ServiceGroupError: CustomStringConvertible { 130 | /// A string representation of the service group error. 131 | public var description: String { 132 | "ServiceGroupError: errorCode: \(self.backing.errorCode), file: \(self.backing.file), line: \(self.backing.line) \(self.backing.message.flatMap { ", message: \($0)" } ?? "")" 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/UnixSignals/UnixSignal.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if canImport(Darwin) 16 | import Darwin 17 | #elseif canImport(Glibc) 18 | import Glibc 19 | #elseif canImport(Musl) 20 | import Musl 21 | #elseif canImport(Android) 22 | import Android 23 | #endif 24 | import Dispatch 25 | 26 | /// A struct representing a Unix signal. 27 | /// 28 | /// Signals are standardized messages sent to a running program to trigger specific behavior, such as quitting or error handling 29 | /// 30 | /// - Important: Unix signals are only functional on platforms supporting signals. 31 | public struct UnixSignal: Hashable, Sendable, CustomStringConvertible { 32 | internal enum Wrapped { 33 | case sigabrt 34 | case sighup 35 | case sigill 36 | case sigint 37 | case sigsegv 38 | case sigterm 39 | case sigusr1 40 | case sigusr2 41 | case sigalrm 42 | case sigquit 43 | case sigwinch 44 | case sigcont 45 | case sigpipe 46 | } 47 | 48 | private let wrapped: Wrapped 49 | private init(_ wrapped: Wrapped) { 50 | self.wrapped = wrapped 51 | } 52 | 53 | public var rawValue: Int32 { 54 | return self.wrapped.rawValue 55 | } 56 | 57 | public var description: String { 58 | return String(describing: self.wrapped) 59 | } 60 | 61 | /// Usually generated by the abort() function. Useful for cleanup prior to termination. 62 | public static let sigabrt = Self(.sigabrt) 63 | /// Hang up detected on controlling terminal or death of controlling process. # ignore-unacceptable-language 64 | public static let sighup = Self(.sighup) 65 | /// Issued if the user attempts to execute an illegal, malformed, or privileged instruction. 66 | public static let sigill = Self(.sigill) 67 | /// Issued if the user sends an interrupt signal. 68 | public static let sigint = Self(.sigint) 69 | /// Issued if the user sends a quit signal. 70 | public static let sigquit = Self(.sigquit) 71 | /// Issued if the user makes an invalid memory reference, such as dereferencing a null or invalid pointer. 72 | public static let sigsegv = Self(.sigsegv) 73 | /// Software termination signal. 74 | public static let sigterm = Self(.sigterm) 75 | public static let sigusr1 = Self(.sigusr1) 76 | public static let sigusr2 = Self(.sigusr2) 77 | public static let sigalrm = Self(.sigalrm) 78 | /// Signal when the window is resized. 79 | public static let sigwinch = Self(.sigwinch) 80 | public static let sigcont = Self(.sigcont) 81 | /// Signal when a write is performed on a closed fd 82 | public static let sigpipe = Self(.sigpipe) 83 | } 84 | 85 | extension UnixSignal.Wrapped: Hashable {} 86 | extension UnixSignal.Wrapped: Sendable {} 87 | 88 | extension UnixSignal.Wrapped: CustomStringConvertible { 89 | var description: String { 90 | switch self { 91 | case .sigabrt: 92 | return "SIGABRT" 93 | case .sighup: 94 | return "SIGHUP" 95 | case .sigill: 96 | return "SIGILL" 97 | case .sigint: 98 | return "SIGINT" 99 | case .sigquit: 100 | return "SIGQUIT" 101 | case .sigsegv: 102 | return "SIGSEGV" 103 | case .sigterm: 104 | return "SIGTERM" 105 | case .sigusr1: 106 | return "SIGUSR1" 107 | case .sigusr2: 108 | return "SIGUSR2" 109 | case .sigalrm: 110 | return "SIGALRM" 111 | case .sigwinch: 112 | return "SIGWINCH" 113 | case .sigcont: 114 | return "SIGCONT" 115 | case .sigpipe: 116 | return "SIGPIPE" 117 | } 118 | } 119 | } 120 | 121 | extension UnixSignal.Wrapped { 122 | var rawValue: Int32 { 123 | #if os(Windows) 124 | return -1 125 | #else 126 | switch self { 127 | case .sigabrt: 128 | return SIGABRT 129 | case .sighup: 130 | return SIGHUP 131 | case .sigill: 132 | return SIGILL 133 | case .sigint: 134 | return SIGINT 135 | case .sigquit: 136 | return SIGQUIT 137 | case .sigsegv: 138 | return SIGSEGV 139 | case .sigterm: 140 | return SIGTERM 141 | case .sigusr1: 142 | return SIGUSR1 143 | case .sigusr2: 144 | return SIGUSR2 145 | case .sigalrm: 146 | return SIGALRM 147 | case .sigwinch: 148 | return SIGWINCH 149 | case .sigcont: 150 | return SIGCONT 151 | case .sigpipe: 152 | return SIGPIPE 153 | } 154 | #endif 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Tests/UnixSignalsTests/UnixSignalTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if !os(Windows) 16 | 17 | import UnixSignals 18 | import XCTest 19 | #if canImport(Darwin) 20 | import Darwin 21 | #elseif canImport(Glibc) 22 | import Glibc 23 | #elseif canImport(Musl) 24 | import Musl 25 | #elseif canImport(Android) 26 | import Android 27 | #endif 28 | 29 | final class UnixSignalTests: XCTestCase { 30 | func testSingleSignal() async throws { 31 | let signal = UnixSignal.sigalrm 32 | let signals = await UnixSignalsSequence(trapping: signal) 33 | let pid = getpid() 34 | 35 | var signalIterator = signals.makeAsyncIterator() 36 | kill(pid, signal.rawValue) // ignore-unacceptable-language 37 | let caught = await signalIterator.next() 38 | XCTAssertEqual(caught, signal) 39 | } 40 | 41 | func testCatchingMultipleSignals() async throws { 42 | let signal = UnixSignal.sigalrm 43 | let signals = await UnixSignalsSequence(trapping: signal) 44 | let pid = getpid() 45 | 46 | var signalIterator = signals.makeAsyncIterator() 47 | for _ in 0..<5 { 48 | kill(pid, signal.rawValue) // ignore-unacceptable-language 49 | 50 | let caught = await signalIterator.next() 51 | XCTAssertEqual(caught, signal) 52 | } 53 | } 54 | 55 | func testCancelOnSignal() async throws { 56 | enum GroupResult { 57 | case timedOut 58 | case cancelled 59 | case caughtSignal 60 | } 61 | 62 | try await withThrowingTaskGroup(of: GroupResult.self) { group in 63 | group.addTask { 64 | do { 65 | try await Task.sleep(nanoseconds: 5_000_000_000) 66 | return .timedOut 67 | } catch { 68 | XCTAssert(error is CancellationError) 69 | return .cancelled 70 | } 71 | } 72 | 73 | group.addTask { 74 | for await _ in await UnixSignalsSequence(trapping: .sigalrm) { 75 | return .caughtSignal 76 | } 77 | fatalError() 78 | } 79 | 80 | // Allow 10ms for the tasks to start. 81 | try await Task.sleep(nanoseconds: 10_000_000) 82 | let pid = getpid() 83 | kill(pid, UnixSignal.sigalrm.rawValue) // ignore-unacceptable-language 84 | 85 | let first = try await group.next() 86 | XCTAssertEqual(first, .caughtSignal) 87 | 88 | // Caught the signal; cancel the remaining task. 89 | group.cancelAll() 90 | let second = try await group.next() 91 | XCTAssertEqual(second, .cancelled) 92 | } 93 | } 94 | 95 | func testEmptySequence() async throws { 96 | let signals = await UnixSignalsSequence(trapping: []) 97 | for await _ in signals { 98 | XCTFail("Unexpected siganl") 99 | } 100 | } 101 | 102 | func testCorrectSignalIsGiven() async throws { 103 | let signals = await UnixSignalsSequence(trapping: .sigterm, .sigusr1, .sigusr2, .sighup, .sigint, .sigalrm) 104 | var signalIterator = signals.makeAsyncIterator() 105 | 106 | let signal = UnixSignal.sigalrm 107 | let pid = getpid() 108 | 109 | for _ in 0..<10 { 110 | kill(pid, signal.rawValue) // ignore-unacceptable-language 111 | let trapped = await signalIterator.next() 112 | XCTAssertEqual(trapped, signal) 113 | } 114 | } 115 | 116 | func testSignalRawValue() { 117 | func assert(_ signal: UnixSignal, rawValue: Int32) { 118 | XCTAssertEqual(signal.rawValue, rawValue) 119 | } 120 | 121 | assert(.sigalrm, rawValue: SIGALRM) 122 | assert(.sigint, rawValue: SIGINT) 123 | assert(.sigquit, rawValue: SIGQUIT) 124 | assert(.sighup, rawValue: SIGHUP) 125 | assert(.sigusr1, rawValue: SIGUSR1) 126 | assert(.sigusr2, rawValue: SIGUSR2) 127 | assert(.sigterm, rawValue: SIGTERM) 128 | } 129 | 130 | func testSignalCustomStringConvertible() { 131 | func assert(_ signal: UnixSignal, description: String) { 132 | XCTAssertEqual(String(describing: signal), description) 133 | } 134 | 135 | assert(.sigalrm, description: "SIGALRM") 136 | assert(.sigint, description: "SIGINT") 137 | assert(.sigquit, description: "SIGQUIT") 138 | assert(.sighup, description: "SIGHUP") 139 | assert(.sigusr1, description: "SIGUSR1") 140 | assert(.sigusr2, description: "SIGUSR2") 141 | assert(.sigterm, description: "SIGTERM") 142 | assert(.sigwinch, description: "SIGWINCH") 143 | } 144 | 145 | func testCancelledTask() async throws { 146 | let task = Task { 147 | try? await Task.sleep(nanoseconds: 1_000_000_000) 148 | 149 | let UnixSignalsSequence = await UnixSignalsSequence(trapping: .sigterm) 150 | 151 | for await _ in UnixSignalsSequence {} 152 | } 153 | 154 | task.cancel() 155 | 156 | await task.value 157 | } 158 | } 159 | 160 | #endif 161 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/AsyncCancelOnGracefulShutdownSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncAlgorithms 16 | 17 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 18 | extension AsyncSequence where Self: Sendable, Element: Sendable { 19 | /// Creates an asynchronous sequence that is cancelled once graceful shutdown has triggered. 20 | /// 21 | /// Use this in places where the only logical thing on graceful shutdown is to cancel your iteration. 22 | public func cancelOnGracefulShutdown() -> AsyncCancelOnGracefulShutdownSequence { 23 | AsyncCancelOnGracefulShutdownSequence(base: self) 24 | } 25 | } 26 | 27 | /// An asynchronous sequence that is cancelled after graceful shutdown has triggered. 28 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 29 | public struct AsyncCancelOnGracefulShutdownSequence: AsyncSequence, Sendable 30 | where Base.Element: Sendable { 31 | @usableFromInline 32 | enum _ElementOrGracefulShutdown: Sendable { 33 | case base(AsyncMapNilSequence.Element) 34 | case gracefulShutdown 35 | } 36 | 37 | @usableFromInline 38 | typealias Merged = AsyncMerge2Sequence< 39 | AsyncMapSequence, _ElementOrGracefulShutdown>, 40 | AsyncMapSequence, _ElementOrGracefulShutdown> 41 | > 42 | 43 | /// The type that the sequence produces. 44 | public typealias Element = Base.Element 45 | 46 | @usableFromInline 47 | let _merge: Merged 48 | 49 | /// Creates a new asynchronous sequence that cancels after graceful shutdown is triggered. 50 | /// - Parameter base: The asynchronous sequence to wrap. 51 | @inlinable 52 | public init(base: Base) { 53 | self._merge = merge( 54 | base.mapNil().map { .base($0) }, 55 | AsyncGracefulShutdownSequence().mapNil().map { _ in .gracefulShutdown } 56 | ) 57 | } 58 | 59 | /// Creates an iterator for the sequence. 60 | @inlinable 61 | public func makeAsyncIterator() -> AsyncIterator { 62 | AsyncIterator(iterator: self._merge.makeAsyncIterator()) 63 | } 64 | 65 | /// An iterator for an asynchronous sequence that cancels after graceful shutdown is triggered. 66 | public struct AsyncIterator: AsyncIteratorProtocol { 67 | @usableFromInline 68 | var _iterator: Merged.AsyncIterator 69 | 70 | @usableFromInline 71 | var _isFinished = false 72 | 73 | @inlinable 74 | init(iterator: Merged.AsyncIterator) { 75 | self._iterator = iterator 76 | } 77 | 78 | /// Returns the next item in the sequence, or `nil` if the sequence is finished. 79 | @inlinable 80 | public mutating func next() async rethrows -> Element? { 81 | guard !self._isFinished else { 82 | return nil 83 | } 84 | 85 | let value = try await self._iterator.next() 86 | 87 | switch value { 88 | case .base(let element): 89 | 90 | switch element { 91 | case .element(let element): 92 | return element 93 | 94 | case .end: 95 | self._isFinished = true 96 | return nil 97 | } 98 | 99 | case .gracefulShutdown: 100 | return nil 101 | 102 | case .none: 103 | return nil 104 | } 105 | } 106 | } 107 | } 108 | 109 | /// This is just a helper extension and sequence to allow us to get the `nil` value as an element of the sequence. 110 | /// We need this since merge is only finishing when both upstreams are finished but we need to finish when either is done. 111 | /// In the future, we should move to something in async algorithms if it exists. 112 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 113 | extension AsyncSequence where Self: Sendable, Element: Sendable { 114 | @inlinable 115 | func mapNil() -> AsyncMapNilSequence { 116 | AsyncMapNilSequence(base: self) 117 | } 118 | } 119 | 120 | @usableFromInline 121 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 122 | struct AsyncMapNilSequence: AsyncSequence, Sendable where Base.Element: Sendable { 123 | @usableFromInline 124 | enum ElementOrEnd: Sendable { 125 | case element(Base.Element) 126 | case end 127 | } 128 | 129 | @usableFromInline 130 | typealias Element = ElementOrEnd 131 | 132 | @usableFromInline 133 | let _base: Base 134 | 135 | @inlinable 136 | init(base: Base) { 137 | self._base = base 138 | } 139 | 140 | @inlinable 141 | func makeAsyncIterator() -> AsyncIterator { 142 | AsyncIterator(iterator: self._base.makeAsyncIterator()) 143 | } 144 | 145 | @usableFromInline 146 | struct AsyncIterator: AsyncIteratorProtocol { 147 | @usableFromInline 148 | var _iterator: Base.AsyncIterator 149 | 150 | @usableFromInline 151 | var _hasSeenEnd = false 152 | 153 | @inlinable 154 | init(iterator: Base.AsyncIterator) { 155 | self._iterator = iterator 156 | } 157 | 158 | @inlinable 159 | mutating func next() async rethrows -> Element? { 160 | let value = try await self._iterator.next() 161 | 162 | if let value { 163 | return .element(value) 164 | } else if self._hasSeenEnd { 165 | return nil 166 | } else { 167 | self._hasSeenEnd = true 168 | return .end 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/Adopting ServiceLifecycle in libraries.md: -------------------------------------------------------------------------------- 1 | # Adopting ServiceLifecycle in libraries 2 | 3 | Adopt the Service protocol for your service to allow a Service Group to coordinate it's operation with other services. 4 | 5 | ## Overview 6 | 7 | Service Lifecycle provides a unified API to represent a long running task: the ``Service`` protocol. 8 | 9 | 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. 10 | 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. 11 | Before Swift Concurrency was introduced, services put their work into separate threads using a `DispatchQueue` or an NIO `EventLoop`. 12 | Services often required explicit lifetime management to make sure their resources, such as threads, were shut down correctly. 13 | 14 | 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. 15 | The `Service` protocol provides a common interface, a single `run()` method, for services to use when they run their long-running work. 16 | If all services in an application conform to this protocol, then orchestrating them becomes trivial. 17 | 18 | ## Adopting the Service protocol in your service 19 | 20 | Adopting the `Service` protocol is quite easy. 21 | The protocol's single requirement is the ``Service/run()`` method. 22 | Make sure that your service adheres to the important caveats addressed in the following sections. 23 | 24 | ### Use Structured Concurrency 25 | 26 | Swift offers multiple ways to use Concurrency. 27 | The primary primitives are the `async` and `await` keywords which enable straight-line code to make asynchronous calls. 28 | The language also provides the concept of task groups; they allow the creation of concurrent work, while staying tied to the parent task. 29 | At the same time, Swift also provides `Task(priority:operation:)` and `Task.detached(priority:operation:)` which create new unstructured Tasks. 30 | 31 | Imagine our library wants to offer a simple `TCPEchoClient`. 32 | To make it Interesting, let's assume we need to send keep-alive pings on every open connection every second. 33 | Below you can see how we could implement this using unstructured concurrency. 34 | 35 | ```swift 36 | public actor TCPEchoClient { 37 | public init() { 38 | Task { 39 | for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) { 40 | self.sendKeepAlivePings() 41 | } 42 | } 43 | } 44 | 45 | private func sendKeepAlivePings() async { ... } 46 | } 47 | ``` 48 | 49 | The above code has a few problems. 50 | First, the code never cancels the `Task` that runs the keep-alive pings. 51 | To do this, it would need to store the `Task` in the actor and cancel it at the appropriate time. 52 | Second, it would also need to expose a `cancel()` method on the actor to cancel the `Task`. 53 | If it were to do all of this, it would have reinvented Structured Concurrency. 54 | 55 | To avoid all of these problems, the code can conform to the ``Service`` protocol. 56 | Its requirement guides us to implement the long-running work inside the `run()` method. 57 | 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. 58 | Furthermore, we now benefit from the automatic cancellation propagation by the task that called our `run()` method. 59 | The code below illustrates an overhauled implementation that exposes such a `run()` method: 60 | 61 | ```swift 62 | public actor TCPEchoClient: Service { 63 | public init() { } 64 | 65 | public func run() async throws { 66 | for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) { 67 | self.sendKeepAlivePings() 68 | } 69 | } 70 | 71 | private func sendKeepAlivePings() async { ... } 72 | } 73 | ``` 74 | 75 | ### Returning from your `run()` method 76 | 77 | 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. 78 | Unless specified otherwise in 79 | ``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` 80 | and 81 | ``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``, 82 | each task started in its respective `run()` method will be canceled. 83 | 84 | ### Cancellation 85 | 86 | Structured Concurrency propagates task cancellation down the task tree. 87 | Every task in the tree can check for cancellation or react to it with cancellation handlers. 88 | ``ServiceGroup`` uses task cancellation to tear down everything when a service’s `run()` method returns early or throws an error. 89 | Hence, it is important that each service properly implements task cancellation in their `run()` methods. 90 | 91 | 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. 92 | The latter is shown in the `TCPEchoClient` example above. 93 | 94 | ### Graceful shutdown 95 | 96 | Applications are often required to be shut down gracefully when run in a real production environment. 97 | 98 | For example, the application might be deployed on Kubernetes and a new version got released. 99 | During a rollout of that new version, Kubernetes sends a `SIGTERM` signal to the application, expecting it to terminate within a grace period. 100 | If the application does not stop in time, then Kubernetes sends the `SIGKILL` signal and forcefully terminates the process. 101 | For this reason, ``ServiceLifecycle`` introduces the _shutdown gracefully_ concept that allows terminating an application’s work in a structured and graceful manner. 102 | 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. 103 | 104 | ``ServiceLifecycle`` exposes one free function called ``withGracefulShutdownHandler(operation:onGracefulShutdown:)`` that works similarly to the `withTaskCancellationHandler` function from the Concurrency library. 105 | Library authors are expected to make sure that any work they spawn from the `run()` method properly supports graceful shutdown. 106 | For example, a server might close its listening socket to stop accepting new connections. 107 | It is of upmost importance that the server does not force the closure of any currently open connections. 108 | It is expected that the business logic behind these connections handles the graceful shutdown. 109 | 110 | An example high-level implementation of a `TCPEchoServer` with graceful shutdown 111 | support might look like this: 112 | 113 | ```swift 114 | public actor TCPEchoServer: Service { 115 | public init() { } 116 | 117 | public func run() async throws { 118 | await withGracefulShutdownHandler { 119 | for connection in self.listeningSocket.connections { 120 | // Handle incoming connections 121 | } 122 | } onGracefulShutdown: { 123 | self.listeningSocket.close() 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | 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. 130 | ``ServiceLifecycle`` provides a convenience on `AsyncSequence` to cancel on graceful shutdown. 131 | Let's take a look at how this works. 132 | 133 | ```swift 134 | public actor TCPEchoClient: Service { 135 | public init() { } 136 | 137 | public func run() async throws { 138 | for await _ in AsyncTimerSequence(interval: .seconds(1), clock: .continuous).cancelOnGracefulShutdown() { 139 | self.sendKeepAlivePings() 140 | } 141 | } 142 | 143 | private func sendKeepAlivePings() async { ... } 144 | } 145 | ``` 146 | 147 | As you can see in the code above, the additional `cancelOnGracefulShutdown()` call takes care of any downstream cancellation. 148 | -------------------------------------------------------------------------------- /Sources/ConcurrencyHelpers/Lock.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | //===----------------------------------------------------------------------===// 16 | // 17 | // This source file is part of the SwiftNIO open source project 18 | // 19 | // Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors 20 | // Licensed under Apache License v2.0 21 | // 22 | // See LICENSE.txt for license information 23 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 24 | // 25 | // SPDX-License-Identifier: Apache-2.0 26 | // 27 | //===----------------------------------------------------------------------===// 28 | 29 | #if canImport(Darwin) 30 | import Darwin 31 | #elseif os(Windows) 32 | import ucrt 33 | import WinSDK 34 | #elseif canImport(Glibc) 35 | import Glibc 36 | #elseif canImport(Musl) 37 | import Musl 38 | #elseif canImport(Android) 39 | import Android 40 | #endif 41 | 42 | #if os(Windows) 43 | @usableFromInline 44 | typealias LockPrimitive = SRWLOCK 45 | #else 46 | @usableFromInline 47 | typealias LockPrimitive = pthread_mutex_t 48 | #endif 49 | 50 | @usableFromInline 51 | enum LockOperations {} 52 | 53 | extension LockOperations { 54 | @inlinable 55 | static func create(_ mutex: UnsafeMutablePointer) { 56 | mutex.assertValidAlignment() 57 | 58 | #if os(Windows) 59 | InitializeSRWLock(mutex) 60 | #else 61 | var attr = pthread_mutexattr_t() 62 | pthread_mutexattr_init(&attr) 63 | 64 | let err = pthread_mutex_init(mutex, &attr) 65 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 66 | #endif 67 | } 68 | 69 | @inlinable 70 | static func destroy(_ mutex: UnsafeMutablePointer) { 71 | mutex.assertValidAlignment() 72 | 73 | #if os(Windows) 74 | // SRWLOCK does not need to be free'd 75 | #else 76 | let err = pthread_mutex_destroy(mutex) 77 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 78 | #endif 79 | } 80 | 81 | @inlinable 82 | static func lock(_ mutex: UnsafeMutablePointer) { 83 | mutex.assertValidAlignment() 84 | 85 | #if os(Windows) 86 | AcquireSRWLockExclusive(mutex) 87 | #else 88 | let err = pthread_mutex_lock(mutex) 89 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 90 | #endif 91 | } 92 | 93 | @inlinable 94 | static func unlock(_ mutex: UnsafeMutablePointer) { 95 | mutex.assertValidAlignment() 96 | 97 | #if os(Windows) 98 | ReleaseSRWLockExclusive(mutex) 99 | #else 100 | let err = pthread_mutex_unlock(mutex) 101 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 102 | #endif 103 | } 104 | } 105 | 106 | // Tail allocate both the mutex and a generic value using ManagedBuffer. 107 | // Both the header pointer and the elements pointer are stable for 108 | // the class's entire lifetime. 109 | // 110 | // However, for safety reasons, we elect to place the lock in the "elements" 111 | // section of the buffer instead of the head. The reasoning here is subtle, 112 | // so buckle in. 113 | // 114 | // _As a practical matter_, the implementation of ManagedBuffer ensures that 115 | // the pointer to the header is stable across the lifetime of the class, and so 116 | // each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` 117 | // the value of the header pointer will be the same. This is because ManagedBuffer uses 118 | // `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure 119 | // that it does not invoke any weird Swift accessors that might copy the value. 120 | // 121 | // _However_, the header is also available via the `.header` field on the ManagedBuffer. 122 | // This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends 123 | // do not interact with Swift's exclusivity model. That is, the various `with` functions do not 124 | // conceptually trigger a mutating access to `.header`. For elements this isn't a concern because 125 | // there's literally no other way to perform the access, but for `.header` it's entirely possible 126 | // to accidentally recursively read it. 127 | // 128 | // Our implementation is free from these issues, so we don't _really_ need to worry about it. 129 | // However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive 130 | // in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, 131 | // and future maintainers will be happier that we were cautious. 132 | // 133 | // See also: https://github.com/apple/swift/pull/40000 134 | @usableFromInline 135 | final class LockStorage: ManagedBuffer { 136 | @inlinable 137 | static func create(value: Value) -> Self { 138 | let buffer = Self.create(minimumCapacity: 1) { _ in 139 | return value 140 | } 141 | // Avoid 'unsafeDowncast' as there is a miscompilation on 5.10. 142 | let storage = buffer as! Self 143 | 144 | storage.withUnsafeMutablePointers { _, lockPtr in 145 | LockOperations.create(lockPtr) 146 | } 147 | 148 | return storage 149 | } 150 | 151 | @inlinable 152 | func lock() { 153 | self.withUnsafeMutablePointerToElements { lockPtr in 154 | LockOperations.lock(lockPtr) 155 | } 156 | } 157 | 158 | @inlinable 159 | func unlock() { 160 | self.withUnsafeMutablePointerToElements { lockPtr in 161 | LockOperations.unlock(lockPtr) 162 | } 163 | } 164 | 165 | @inlinable 166 | deinit { 167 | self.withUnsafeMutablePointerToElements { lockPtr in 168 | LockOperations.destroy(lockPtr) 169 | } 170 | } 171 | 172 | @inlinable 173 | func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { 174 | try self.withUnsafeMutablePointerToElements { lockPtr in 175 | return try body(lockPtr) 176 | } 177 | } 178 | 179 | @inlinable 180 | func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { 181 | try self.withUnsafeMutablePointers { valuePtr, lockPtr in 182 | LockOperations.lock(lockPtr) 183 | defer { LockOperations.unlock(lockPtr) } 184 | return try mutate(&valuePtr.pointee) 185 | } 186 | } 187 | } 188 | 189 | /// A threading lock based on `libpthread` instead of `libdispatch`. 190 | /// 191 | /// - note: ``Lock`` has reference semantics. 192 | /// 193 | /// This object provides a lock on top of a single `pthread_mutex_t`. This kind 194 | /// of lock is safe to use with `libpthread`-based threading models, such as the 195 | /// one used by . On Windows, the lock is based on the substantially similar 196 | /// `SRWLOCK` type. 197 | public struct Lock { 198 | @usableFromInline 199 | internal let _storage: LockStorage 200 | 201 | /// Create a new lock. 202 | @inlinable 203 | public init() { 204 | self._storage = .create(value: ()) 205 | } 206 | 207 | /// Acquire the lock. 208 | /// 209 | /// Whenever possible, consider using `withLock` instead of this method and 210 | /// `unlock`, to simplify lock handling. 211 | @inlinable 212 | public func lock() { 213 | self._storage.lock() 214 | } 215 | 216 | /// Release the lock. 217 | /// 218 | /// Whenever possible, consider using `withLock` instead of this method and 219 | /// `lock`, to simplify lock handling. 220 | @inlinable 221 | public func unlock() { 222 | self._storage.unlock() 223 | } 224 | 225 | @inlinable 226 | internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { 227 | return try self._storage.withLockPrimitive(body) 228 | } 229 | } 230 | 231 | extension Lock { 232 | /// Acquire the lock for the duration of the given block. 233 | /// 234 | /// This convenience method should be preferred to `lock` and `unlock` in 235 | /// most situations, as it ensures that the lock will be released regardless 236 | /// of how `body` exits. 237 | /// 238 | /// - Parameter body: The block to execute while holding the lock. 239 | /// - Returns: The value returned by the block. 240 | @inlinable 241 | public func withLock(_ body: () throws -> T) rethrows -> T { 242 | self.lock() 243 | defer { 244 | self.unlock() 245 | } 246 | return try body() 247 | } 248 | 249 | @inlinable 250 | public func withLockVoid(_ body: () throws -> Void) rethrows { 251 | try self.withLock(body) 252 | } 253 | } 254 | 255 | extension Lock: @unchecked Sendable {} 256 | 257 | extension UnsafeMutablePointer { 258 | @inlinable 259 | func assertValidAlignment() { 260 | assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/Docs.docc/Adopting ServiceLifecycle in applications.md: -------------------------------------------------------------------------------- 1 | # Adopting ServiceLifecycle in applications 2 | 3 | Service Lifecycle provides a unified API for services to streamline their 4 | orchestration in applications: the Service Group actor. 5 | 6 | ## Overview 7 | 8 | Applications often rely on fundamental observability services like logging and 9 | metrics, while long-running actors bundle the application's business logic in 10 | their services. There might also exist HTTP, gRPC, or similar servers exposed by 11 | the application. It is therefore a strong requirement for the application to 12 | orchestrate the various services during startup and shutdown. 13 | 14 | With the introduction of Structured Concurrency in Swift, multiple asynchronous 15 | services can be run concurrently with task groups. However, Structured 16 | Concurrency doesn't enforce consistent interfaces between services, and it can 17 | become hard to orchestrate them. To solve this issue, ``ServiceLifecycle`` 18 | provides the ``Service`` protocol to enforce a common API, as well as the 19 | ``ServiceGroup`` actor to orchestrate all services in an application. 20 | 21 | ## Adopting the ServiceGroup actor in your application 22 | 23 | This article focuses on how ``ServiceGroup`` works, and how you can adopt it in 24 | your application. If you are interested in how to properly implement a service, 25 | go check out the article: . 26 | 27 | ### How does the ServiceGroup actor work? 28 | 29 | Under the hood, the ``ServiceGroup`` actor is just a complicated task group that 30 | runs each service in a separate child task, and handles individual services 31 | exiting or throwing. It also introduces the concept of graceful shutdown, which 32 | allows the safe teardown of all services in reverse order. Graceful shutdown is 33 | often used in server scenarios, for example when rolling out a new version and 34 | draining traffic from the old version (commonly referred to as quiescing). 35 | 36 | ### How to use ServiceGroup? 37 | 38 | Let's take a look at how ``ServiceGroup`` can be used in an application. First, we 39 | define some fictional services. 40 | 41 | ```swift 42 | struct FooService: Service { 43 | func run() async throws { ... } 44 | } 45 | 46 | public struct BarService: Service { 47 | private let fooService: FooService 48 | 49 | init(fooService: FooService) { 50 | self.fooService = fooService 51 | } 52 | 53 | func run() async throws { ... } 54 | } 55 | ``` 56 | 57 | In our example, `BarService` depends on `FooService`. A dependency between 58 | services is very common and ``ServiceGroup`` infers the dependencies from the 59 | order that services are passed to in ``ServiceGroup/init(configuration:)``. 60 | Services with a higher index can depend on services with a lower index. The 61 | following example shows how this can be applied to our `BarService`. 62 | 63 | ```swift 64 | import ServiceLifecycle 65 | import Logging 66 | 67 | @main 68 | struct Application { 69 | static let logger = Logger(label: "Application") 70 | 71 | static func main() async throws { 72 | let fooService = FooServer() 73 | let barService = BarService(fooService: fooService) 74 | 75 | let serviceGroup = ServiceGroup( 76 | // We encode the dependency hierarchy by putting fooService first 77 | services: [fooService, barService], 78 | logger: logger 79 | ) 80 | 81 | try await serviceGroup.run() 82 | } 83 | } 84 | ``` 85 | 86 | ### Graceful shutdown 87 | 88 | Graceful shutdown is a concept introduced in ServiceLifecycle, with the aim to 89 | be a less forceful alternative to task cancellation. Graceful shutdown allows 90 | each service to opt-in support. For example, you might want to use graceful 91 | shutdown in containerized environments such as Docker or Kubernetes. In those 92 | environments, `SIGTERM` is commonly used to indicate that the application should 93 | shutdown. If it does not, then a `SIGKILL` is sent to force a non-graceful 94 | shutdown. 95 | 96 | The ``ServiceGroup`` can be set up to listen to `SIGTERM` and trigger a graceful 97 | shutdown on all its orchestrated services. Once the signal is received, it will 98 | gracefully shut down each service one by one in reverse startup order. 99 | Importantly, the ``ServiceGroup`` is going to wait for the ``Service/run()`` 100 | method to return before triggering the graceful shutdown of the next service. 101 | 102 | We recommend both application and service authors to make sure that their 103 | implementations handle graceful shutdowns correctly. The following is an example 104 | application that implements a streaming service, but does not support a graceful 105 | shutdown of either the service or application. 106 | 107 | ```swift 108 | import ServiceLifecycle 109 | import Logging 110 | 111 | struct StreamingService: Service { 112 | struct RequestStream: AsyncSequence { ... } 113 | struct ResponseWriter { 114 | func write() 115 | } 116 | 117 | private let streamHandler: (RequestStream, ResponseWriter) async -> Void 118 | 119 | init(streamHandler: @escaping (RequestStream, ResponseWriter) async -> Void) { 120 | self.streamHandler = streamHandler 121 | } 122 | 123 | func run() async throws { 124 | await withDiscardingTaskGroup { group in 125 | for stream in makeStreams() { 126 | group.addTask { 127 | await streamHandler(stream.requestStream, stream.responseWriter) 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | @main 135 | struct Application { 136 | static let logger = Logger(label: "Application") 137 | 138 | static func main() async throws { 139 | let streamingService = StreamingService(streamHandler: { requestStream, responseWriter in 140 | for await request in requestStream { 141 | responseWriter.write("response") 142 | } 143 | }) 144 | 145 | let serviceGroup = ServiceGroup( 146 | services: [streamingService], 147 | gracefulShutdownSignals: [.sigterm], 148 | logger: logger 149 | ) 150 | 151 | try await serviceGroup.run() 152 | } 153 | } 154 | ``` 155 | 156 | The code above demonstrates a hypothetical `StreamingService` with one 157 | configurable handler invoked per stream. Each stream is handled concurrently in 158 | a separate child task. The above code doesn't support graceful shutdown right 159 | now, and it has to be added in two places. First, the service's `run()` method 160 | iterates the `makeStream()` async sequence. This iteration is not stopped on a 161 | graceful shutdown, and we continue to accept new streams. Also, the 162 | `streamHandler` that we pass in our main method does not support graceful 163 | shutdown, since it iterates over the incoming requests. 164 | 165 | Luckily, adding support in both places is trivial with the helpers that 166 | ``ServiceLifecycle`` exposes. In both cases, we iterate over an async sequence 167 | and we want to stop iteration for a graceful shutdown. To do this, we can use 168 | the `cancelOnGracefulShutdown()` method that ``ServiceLifecycle`` adds to 169 | `AsyncSequence`. The updated code looks like this: 170 | 171 | ```swift 172 | import ServiceLifecycle 173 | import Logging 174 | 175 | struct StreamingService: Service { 176 | struct RequestStream: AsyncSequence { ... } 177 | struct ResponseWriter { 178 | func write() 179 | } 180 | 181 | private let streamHandler: (RequestStream, ResponseWriter) async -> Void 182 | 183 | init(streamHandler: @escaping (RequestStream, ResponseWriter) async -> Void) { 184 | self.streamHandler = streamHandler 185 | } 186 | 187 | func run() async throws { 188 | await withDiscardingTaskGroup { group in 189 | for stream in makeStreams().cancelOnGracefulShutdown() { 190 | group.addTask { 191 | await streamHandler(stream.requestStream, stream.responseWriter) 192 | } 193 | } 194 | } 195 | } 196 | } 197 | 198 | @main 199 | struct Application { 200 | static let logger = Logger(label: "Application") 201 | 202 | static func main() async throws { 203 | let streamingService = StreamingService(streamHandler: { requestStream, responseWriter in 204 | for await request in requestStream.cancelOnGracefulShutdown() { 205 | responseWriter.write("response") 206 | } 207 | }) 208 | 209 | let serviceGroup = ServiceGroup( 210 | services: [streamingService], 211 | gracefulShutdownSignals: [.sigterm],, 212 | logger: logger 213 | ) 214 | 215 | try await serviceGroup.run() 216 | } 217 | } 218 | ``` 219 | 220 | A valid question to ask here is why we are not using cancellation in the first 221 | place? The problem is that cancellation is forceful and does not allow users to 222 | make a decision if they want to stop a process or not. However, a graceful 223 | shutdown is often very specific to business logic. In our case, we were fine 224 | with just stopping the handling of new requests on a stream. Other applications 225 | might want to send a response to indicate to the client that the server is 226 | shutting down, and await an acknowledgment of that message. 227 | 228 | ### Customizing the behavior when a service returns or throws 229 | 230 | By default, the ``ServiceGroup`` cancels the whole group if one service returns 231 | or throws. However, in some scenarios, this is unexpected, e.g., when the 232 | ``ServiceGroup`` is used in a CLI to orchestrate some services while a command 233 | is handled. To customize the behavior, you set the 234 | ``ServiceGroupConfiguration/ServiceConfiguration/successTerminationBehavior`` 235 | and 236 | ``ServiceGroupConfiguration/ServiceConfiguration/failureTerminationBehavior``. 237 | Both of them offer three different options. The default behavior for both is 238 | ``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/cancelGroup``. 239 | You can also choose to either ignore that a service returns/throws, by setting 240 | it to 241 | ``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/ignore`` or 242 | trigger a graceful shutdown by setting it to 243 | ``ServiceGroupConfiguration/ServiceConfiguration/TerminationBehavior/gracefullyShutdownGroup``. 244 | 245 | Another example where you might want to customize the behavior is when you 246 | have a service that should be gracefully shut down when another service exits. 247 | For example, you want to make sure your telemetry service is gracefully shut down 248 | after your HTTP server unexpectedly throws from its `run()` method. This setup 249 | could look like this: 250 | 251 | ```swift 252 | import ServiceLifecycle 253 | import Logging 254 | 255 | @main 256 | struct Application { 257 | static let logger = Logger(label: "Application") 258 | 259 | static func main() async throws { 260 | let telemetryService = TelemetryService() 261 | let httpServer = HTTPServer() 262 | 263 | let serviceGroup = ServiceGroup( 264 | configuration: .init( 265 | services: [ 266 | .init(service: telemetryService), 267 | .init( 268 | service: httpServer, 269 | successTerminationBehavior: .shutdownGracefully, 270 | failureTerminationBehavior: .shutdownGracefully 271 | ) 272 | ], 273 | logger: logger 274 | ), 275 | ) 276 | 277 | try await serviceGroup.run() 278 | } 279 | } 280 | ``` 281 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/ServiceGroupConfiguration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Logging 16 | import UnixSignals 17 | 18 | let deprecatedLoggerLabel = "service-lifecycle-deprecated-method-logger" 19 | 20 | /// The configuration for the service group. 21 | /// 22 | /// 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. 23 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 24 | public struct ServiceGroupConfiguration: Sendable { 25 | /// The group's logging configuration. 26 | public struct LoggingConfiguration: Sendable { 27 | /// The keys to use for logging 28 | public struct Keys: Sendable { 29 | /// The logging key used for logging the unix signal. 30 | public var signalKey = "sl-signal" 31 | /// The logging key used for logging the graceful shutdown unix signals. 32 | public var gracefulShutdownSignalsKey = "sl-gracefulShutdownSignals" 33 | /// The logging key used for logging the cancellation unix signals. 34 | public var cancellationSignalsKey = "sl-cancellationSignals" 35 | /// The logging key used for logging the service. 36 | public var serviceKey = "sl-service" 37 | /// The logging key used for logging the services. 38 | public var servicesKey = "sl-services" 39 | /// The logging key used for logging an error. 40 | public var errorKey = "sl-error" 41 | 42 | /// Creates a new set of keys. 43 | public init() {} 44 | } 45 | 46 | /// The keys used for logging. 47 | public var keys = Keys() 48 | 49 | /// Creates a new logging configuration. 50 | public init() {} 51 | } 52 | 53 | /// A service configuration. 54 | public struct ServiceConfiguration: Sendable { 55 | 56 | /// The behavior to follow when the service finishes running. 57 | /// 58 | /// This describes what the service lifecycle code does when a service's run method returns or throws. 59 | public struct TerminationBehavior: Sendable, CustomStringConvertible { 60 | internal enum _TerminationBehavior { 61 | case cancelGroup 62 | case gracefullyShutdownGroup 63 | case ignore 64 | } 65 | 66 | internal let behavior: _TerminationBehavior 67 | 68 | /// Cancel the service group. 69 | public static let cancelGroup = Self(behavior: .cancelGroup) 70 | /// Gracefully shut down the service group. 71 | public static let gracefullyShutdownGroup = Self(behavior: .gracefullyShutdownGroup) 72 | /// Ignore the completion of the service. 73 | public static let ignore = Self(behavior: .ignore) 74 | 75 | /// A string representation of the behavior when a service finishes running. 76 | public var description: String { 77 | switch self.behavior { 78 | case .cancelGroup: 79 | return "cancelGroup" 80 | case .gracefullyShutdownGroup: 81 | return "gracefullyShutdownGroup" 82 | case .ignore: 83 | return "ignore" 84 | } 85 | } 86 | } 87 | 88 | /// The service to which the initialized configuration applies. 89 | public var service: any Service 90 | /// The behavior when the service returns from its run method. 91 | public var successTerminationBehavior: TerminationBehavior 92 | /// The behavior when the service throws from its run method. 93 | public var failureTerminationBehavior: TerminationBehavior 94 | 95 | /// Initializes a new service configuration. 96 | /// 97 | /// - Parameters: 98 | /// - service: The service to which the initialized configuration applies. 99 | /// - successTerminationBehavior: The behavior when the service returns from its run method. 100 | /// - failureTerminationBehavior: The behavior when the service throws from its run method. 101 | public init( 102 | service: any Service, 103 | successTerminationBehavior: TerminationBehavior = .cancelGroup, 104 | failureTerminationBehavior: TerminationBehavior = .cancelGroup 105 | ) { 106 | self.service = service 107 | self.successTerminationBehavior = successTerminationBehavior 108 | self.failureTerminationBehavior = failureTerminationBehavior 109 | } 110 | } 111 | 112 | /// The groups's service configurations. 113 | public var services: [ServiceConfiguration] 114 | 115 | /// The signals that lead to graceful shutdown. 116 | public var gracefulShutdownSignals = [UnixSignal]() 117 | 118 | /// The signals that lead to cancellation. 119 | public var cancellationSignals = [UnixSignal]() 120 | 121 | /// The group's logger. 122 | public var logger: Logger 123 | 124 | /// The group's logging configuration. 125 | public var logging = LoggingConfiguration() 126 | 127 | /// The maximum amount of time that graceful shutdown is allowed to take. 128 | /// 129 | /// After this time has elapsed graceful shutdown will be escalated to task cancellation. 130 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 131 | public var maximumGracefulShutdownDuration: Duration? { 132 | get { 133 | guard let maximumGracefulShutdownDuration = self._maximumGracefulShutdownDuration else { 134 | return nil 135 | } 136 | return .init( 137 | secondsComponent: maximumGracefulShutdownDuration.secondsComponent, 138 | attosecondsComponent: maximumGracefulShutdownDuration.attosecondsComponent 139 | ) 140 | } 141 | set { 142 | if let newValue = newValue { 143 | self._maximumGracefulShutdownDuration = (newValue.components.seconds, newValue.components.attoseconds) 144 | } else { 145 | self._maximumGracefulShutdownDuration = nil 146 | } 147 | } 148 | } 149 | 150 | internal var _maximumGracefulShutdownDuration: (secondsComponent: Int64, attosecondsComponent: Int64)? 151 | 152 | /// The maximum amount of time that task cancellation is allowed to take. 153 | /// 154 | /// After this time has elapsed task cancellation will be escalated to a `fatalError`. 155 | /// 156 | /// - Important: This setting is useful to guarantee that your application will exit at some point and 157 | /// should be used to identify APIs that are not properly implementing task cancellation. 158 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 159 | public var maximumCancellationDuration: Duration? { 160 | get { 161 | guard let maximumCancellationDuration = self._maximumCancellationDuration else { 162 | return nil 163 | } 164 | return .init( 165 | secondsComponent: maximumCancellationDuration.secondsComponent, 166 | attosecondsComponent: maximumCancellationDuration.attosecondsComponent 167 | ) 168 | } 169 | set { 170 | if let newValue = newValue { 171 | self._maximumCancellationDuration = (newValue.components.seconds, newValue.components.attoseconds) 172 | } else { 173 | self._maximumCancellationDuration = nil 174 | } 175 | } 176 | } 177 | 178 | internal var _maximumCancellationDuration: (secondsComponent: Int64, attosecondsComponent: Int64)? 179 | 180 | /// Creates a new service group configuration from the service configurations you provide. 181 | /// 182 | /// - Parameters: 183 | /// - services: The groups's service configurations. 184 | /// - logger: The group's logger. 185 | public init( 186 | services: [ServiceConfiguration], 187 | logger: Logger 188 | ) { 189 | self.services = services 190 | self.logger = logger 191 | } 192 | 193 | /// Creates a new service group configuration from service configurations you provide with cancellation and shutdown signals. 194 | /// 195 | /// - Parameters: 196 | /// - services: The groups's service configurations. 197 | /// - gracefulShutdownSignals: The signals that lead to graceful shutdown. 198 | /// - cancellationSignals: The signals that lead to cancellation. 199 | /// - logger: The group's logger. 200 | public init( 201 | services: [ServiceConfiguration], 202 | gracefulShutdownSignals: [UnixSignal] = [], 203 | cancellationSignals: [UnixSignal] = [], 204 | logger: Logger 205 | ) { 206 | self.services = services 207 | self.logger = logger 208 | self.gracefulShutdownSignals = gracefulShutdownSignals 209 | self.cancellationSignals = cancellationSignals 210 | } 211 | 212 | /// Creates a new service group configuration from services you provide. 213 | /// 214 | /// - Parameters: 215 | /// - services: The groups's services. 216 | /// - logger: The group's logger. 217 | public init( 218 | services: [Service], 219 | logger: Logger 220 | ) { 221 | self.services = Array(services.map { ServiceConfiguration(service: $0) }) 222 | self.logger = logger 223 | } 224 | 225 | /// Creates a new service group configuration from services you provide with cancellation and shutdown signals.. 226 | /// 227 | /// - Parameters: 228 | /// - services: The groups's services. 229 | /// - gracefulShutdownSignals: The signals that lead to graceful shutdown. 230 | /// - cancellationSignals: The signals that lead to cancellation. 231 | /// - logger: The group's logger. 232 | public init( 233 | services: [Service], 234 | gracefulShutdownSignals: [UnixSignal] = [], 235 | cancellationSignals: [UnixSignal] = [], 236 | logger: Logger 237 | ) { 238 | self.services = Array(services.map { ServiceConfiguration(service: $0) }) 239 | self.logger = logger 240 | self.gracefulShutdownSignals = gracefulShutdownSignals 241 | self.cancellationSignals = cancellationSignals 242 | } 243 | 244 | /// Creates a new service group configuration. 245 | /// 246 | /// Use ``init(services:gracefulShutdownSignals:cancellationSignals:logger:)-1noxs`` 247 | /// or ``init(services:gracefulShutdownSignals:cancellationSignals:logger:)-9uhzu`` instead. 248 | @available(*, deprecated) 249 | public init(gracefulShutdownSignals: [UnixSignal]) { 250 | self.services = [] 251 | self.gracefulShutdownSignals = gracefulShutdownSignals 252 | self.logger = Logger(label: deprecatedLoggerLabel) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Sources/UnixSignals/UnixSignalsSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if canImport(Darwin) 16 | import Darwin 17 | import Dispatch 18 | #else 19 | @preconcurrency import Dispatch 20 | #endif 21 | 22 | #if canImport(Glibc) 23 | import Glibc 24 | #elseif canImport(Musl) 25 | import Musl 26 | #elseif canImport(Android) 27 | import Android 28 | #endif 29 | import ConcurrencyHelpers 30 | 31 | /// An unterminated `AsyncSequence` of ``UnixSignal``s. 32 | /// 33 | /// This can be used to setup signal handlers and receive the signals on the sequence. 34 | /// 35 | /// - Important: There can only be a single signal handler for a signal installed. So you should avoid creating multiple handlers 36 | /// for the same signal. 37 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 38 | public struct UnixSignalsSequence: AsyncSequence, Sendable { 39 | private static let queue = DispatchQueue(label: "com.service-lifecycle.unix-signals") 40 | 41 | public typealias Element = UnixSignal 42 | 43 | fileprivate struct Source: Sendable { 44 | var dispatchSource: DispatchSource 45 | var signal: UnixSignal 46 | } 47 | 48 | private let storage: Storage 49 | 50 | public init(trapping signals: UnixSignal...) async { 51 | await self.init(trapping: signals) 52 | } 53 | 54 | public init(trapping signals: [UnixSignal]) async { 55 | // We are converting the signals to a Set here to remove duplicates 56 | self.storage = await .init(signals: Set(signals)) 57 | } 58 | 59 | public func makeAsyncIterator() -> AsyncIterator { 60 | return .init(iterator: self.storage.makeAsyncIterator(), storage: self.storage) 61 | } 62 | 63 | public struct AsyncIterator: AsyncIteratorProtocol { 64 | private var iterator: AsyncStream.Iterator 65 | // We need to keep the underlying `DispatchSourceSignal` alive to receive notifications. 66 | private let storage: Storage 67 | 68 | fileprivate init(iterator: AsyncStream.Iterator, storage: Storage) { 69 | self.iterator = iterator 70 | self.storage = storage 71 | } 72 | 73 | public mutating func next() async -> UnixSignal? { 74 | return await self.iterator.next() 75 | } 76 | } 77 | } 78 | 79 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 80 | extension UnixSignalsSequence { 81 | fileprivate final class Storage: @unchecked Sendable { 82 | private let stateMachine: LockedValueBox 83 | 84 | init(signals: Set) async { 85 | #if !os(Windows) 86 | let sources: [Source] = signals.compactMap { sig -> Source? in 87 | #if canImport(Darwin) 88 | // On Darwin platforms Dispatch's signal source uses kqueue and EVFILT_SIGNAL for 89 | // delivering signals. This exists alongside but with lower precedence than signal and 90 | // sigaction: ignore signal handling here to kqueue can deliver signals. 91 | signal(sig.rawValue, SIG_IGN) 92 | #endif 93 | return .init( 94 | // This force-unwrap is safe since Dispatch always returns a `DispatchSource` 95 | dispatchSource: DispatchSource.makeSignalSource( 96 | signal: sig.rawValue, 97 | queue: UnixSignalsSequence.queue 98 | ) as! DispatchSource, 99 | signal: sig 100 | ) 101 | } 102 | 103 | let stream = AsyncStream { continuation in 104 | for source in sources { 105 | source.dispatchSource.setEventHandler { 106 | continuation.yield(source.signal) 107 | } 108 | source.dispatchSource.setCancelHandler { 109 | continuation.finish() 110 | } 111 | } 112 | 113 | // Don't wait forever if there's nothing to wait for. 114 | if sources.isEmpty { 115 | continuation.finish() 116 | } 117 | } 118 | 119 | self.stateMachine = .init(.init(sources: sources, stream: stream)) 120 | 121 | // Registering sources is async: await their registration so we don't miss early signals. 122 | await withTaskCancellationHandler { 123 | for source in sources { 124 | await withCheckedContinuation { (continuation: CheckedContinuation) in 125 | let action = self.stateMachine.withLockedValue { 126 | $0.registeringSignal(continuation: continuation) 127 | } 128 | switch action { 129 | case .setRegistrationHandlerAndResumeDispatchSource: 130 | source.dispatchSource.setRegistrationHandler { 131 | let action = self.stateMachine.withLockedValue { $0.registeredSignal() } 132 | 133 | switch action { 134 | case .none: 135 | break 136 | 137 | case .resumeContinuation(let continuation): 138 | continuation.resume() 139 | } 140 | } 141 | source.dispatchSource.resume() 142 | 143 | case .resumeContinuation(let continuation): 144 | continuation.resume() 145 | } 146 | } 147 | } 148 | } onCancel: { 149 | let action = self.stateMachine.withLockedValue { $0.cancelledInit() } 150 | 151 | switch action { 152 | case .none: 153 | break 154 | 155 | case .cancelAllSources: 156 | for source in sources { 157 | source.dispatchSource.cancel() 158 | } 159 | 160 | case .resumeContinuationAndCancelAllSources(let continuation): 161 | continuation.resume() 162 | 163 | for source in sources { 164 | source.dispatchSource.cancel() 165 | } 166 | } 167 | } 168 | #else 169 | let stream = AsyncStream { _ in } 170 | self.stateMachine = .init(.init(sources: [], stream: stream)) 171 | #endif 172 | } 173 | 174 | func makeAsyncIterator() -> AsyncStream.AsyncIterator { 175 | return self.stateMachine.withLockedValue { $0.makeAsyncIterator() } 176 | } 177 | } 178 | } 179 | 180 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 181 | extension UnixSignalsSequence { 182 | fileprivate struct StateMachine { 183 | private enum State { 184 | /// The initial state. 185 | case canRegisterSignal( 186 | sources: [Source], 187 | stream: AsyncStream 188 | ) 189 | /// The state once we started to register a signal handler. 190 | /// 191 | /// - Note: We can enter this state multiple times. One for each signal handler. 192 | case registeringSignal( 193 | sources: [Source], 194 | stream: AsyncStream, 195 | continuation: CheckedContinuation 196 | ) 197 | /// The state once an iterator has been created. 198 | case producing( 199 | sources: [Source], 200 | stream: AsyncStream 201 | ) 202 | /// The state when the task that creates the UnixSignals gets cancelled during init. 203 | case cancelled( 204 | stream: AsyncStream 205 | ) 206 | } 207 | 208 | private var state: State 209 | 210 | init(sources: [Source], stream: AsyncStream) { 211 | self.state = .canRegisterSignal(sources: sources, stream: stream) 212 | } 213 | 214 | enum RegisteringSignalAction { 215 | case setRegistrationHandlerAndResumeDispatchSource 216 | case resumeContinuation(CheckedContinuation) 217 | } 218 | 219 | mutating func registeringSignal(continuation: CheckedContinuation) -> RegisteringSignalAction { 220 | switch self.state { 221 | case .canRegisterSignal(let sources, let stream): 222 | self.state = .registeringSignal(sources: sources, stream: stream, continuation: continuation) 223 | 224 | return .setRegistrationHandlerAndResumeDispatchSource 225 | 226 | case .registeringSignal: 227 | fatalError("UnixSignals tried to register multiple signals at once") 228 | 229 | case .producing: 230 | fatalError("UnixSignals tried to create an iterator before the init was done") 231 | 232 | case .cancelled: 233 | return .resumeContinuation(continuation) 234 | } 235 | } 236 | 237 | enum RegisteredSignalAction { 238 | case resumeContinuation(CheckedContinuation) 239 | } 240 | 241 | mutating func registeredSignal() -> RegisteredSignalAction? { 242 | switch self.state { 243 | case .canRegisterSignal: 244 | fatalError("UnixSignals tried to register signals more than once") 245 | 246 | case .registeringSignal(let sources, let stream, let continuation): 247 | self.state = .canRegisterSignal(sources: sources, stream: stream) 248 | 249 | return .resumeContinuation(continuation) 250 | 251 | case .producing: 252 | fatalError("UnixSignals tried to create an iterator before the init was done") 253 | 254 | case .cancelled: 255 | // This is okay. The registration and cancelling the source might race. 256 | return .none 257 | } 258 | } 259 | 260 | enum CancelledInitAction { 261 | case cancelAllSources 262 | case resumeContinuationAndCancelAllSources(CheckedContinuation) 263 | } 264 | 265 | mutating func cancelledInit() -> CancelledInitAction? { 266 | switch self.state { 267 | case .canRegisterSignal(_, let stream): 268 | self.state = .cancelled(stream: stream) 269 | 270 | return .cancelAllSources 271 | 272 | case .registeringSignal(_, let stream, let continuation): 273 | self.state = .cancelled(stream: stream) 274 | 275 | return .resumeContinuationAndCancelAllSources(continuation) 276 | 277 | case .producing: 278 | // This is a weird one. The task that created the UnixSignals 279 | // got cancelled but we already made an iterator. I guess 280 | // this can happen while we are racing so let's just tolerate that 281 | // and do nothing. If the task that created the UnixSignals and is 282 | // consuming it is the same one then the stream will terminate anyhow. 283 | return .none 284 | 285 | case .cancelled: 286 | fatalError("UnixSignals registration cancelled more than once") 287 | } 288 | } 289 | 290 | mutating func makeAsyncIterator() -> AsyncStream.AsyncIterator { 291 | switch self.state { 292 | case .canRegisterSignal(let sources, let stream): 293 | // This can happen when we created a UnixSignal without any signals 294 | self.state = .producing(sources: sources, stream: stream) 295 | 296 | return stream.makeAsyncIterator() 297 | 298 | case .registeringSignal: 299 | fatalError("UnixSignals tried to create iterator before all handlers were installed") 300 | 301 | case .producing: 302 | fatalError("UnixSignals only allows a single iterator to be created") 303 | 304 | case .cancelled(let stream): 305 | return stream.makeAsyncIterator() 306 | } 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /Sources/ServiceLifecycle/GracefulShutdown.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ConcurrencyHelpers 16 | 17 | #if compiler(>=6.0) 18 | /// Execute an operation with a graceful shutdown handler that’s immediately invoked if the current task is shutting down gracefully. 19 | /// 20 | /// This doesn’t check for graceful shutdown, and always executes the passed operation. 21 | /// The operation executes on the calling execution context and does not suspend by itself, unless the code contained within the closure does. 22 | /// If graceful shutdown occurs while the operation is running, the graceful shutdown handler will execute concurrently with the operation. 23 | /// 24 | /// When `withGracefulShutdownHandler` is used in a Task that has already been gracefully shutdown, the `onGracefulShutdown` handler 25 | /// will be executed immediately before operation gets to execute. This allows the `onGracefulShutdown` handler to set some external “shutdown” flag 26 | /// that the operation may be atomically checking for in order to avoid performing any actual work once the operation gets to run. 27 | /// 28 | /// A common use-case is to listen to graceful shutdown and use the `ServerQuiescingHelper` from `swift-nio-extras` to 29 | /// trigger the quiescing sequence. Furthermore, graceful shutdown will propagate to any child task that is currently executing 30 | /// 31 | /// - Important: This method will only set up a handler if run inside ``ServiceGroup`` otherwise no graceful shutdown handler 32 | /// will be set up. 33 | /// 34 | /// - Parameters: 35 | /// - isolation: The isolation of the method. Defaults to the isolation of the caller. 36 | /// - operation: The actual operation. 37 | /// - handler: The handler which is invoked once graceful shutdown has been triggered. 38 | // Unsafely inheriting the executor is safe to do here since we are not calling any other async method 39 | // except the operation. This makes sure no other executor hops would occur here. 40 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 41 | public func withGracefulShutdownHandler( 42 | isolation: isolated (any Actor)? = #isolation, 43 | operation: () async throws -> T, 44 | onGracefulShutdown handler: @Sendable @escaping () -> Void 45 | ) async rethrows -> T { 46 | guard let gracefulShutdownManager = TaskLocals.gracefulShutdownManager else { 47 | return try await operation() 48 | } 49 | 50 | // We have to keep track of our handler here to remove it once the operation is finished. 51 | let handlerID = gracefulShutdownManager.registerHandler(handler) 52 | defer { 53 | if let handlerID = handlerID { 54 | gracefulShutdownManager.removeHandler(handlerID) 55 | } 56 | } 57 | 58 | return try await operation() 59 | } 60 | 61 | /// Execute an operation with a graceful shutdown handler that’s immediately invoked if the current task is shutting down gracefully. 62 | /// 63 | /// Use ``withGracefulShutdownHandler(isolation:operation:onGracefulShutdown:)`` instead. 64 | @available(*, deprecated, message: "Use the method with the isolation parameter instead.") 65 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 66 | @_disfavoredOverload 67 | public func withGracefulShutdownHandler( 68 | operation: () async throws -> T, 69 | onGracefulShutdown handler: @Sendable @escaping () -> Void 70 | ) async rethrows -> T { 71 | guard let gracefulShutdownManager = TaskLocals.gracefulShutdownManager else { 72 | return try await operation() 73 | } 74 | 75 | // We have to keep track of our handler here to remove it once the operation is finished. 76 | let handlerID = gracefulShutdownManager.registerHandler(handler) 77 | defer { 78 | if let handlerID = handlerID { 79 | gracefulShutdownManager.removeHandler(handlerID) 80 | } 81 | } 82 | 83 | return try await operation() 84 | } 85 | 86 | #else 87 | // We need to retain this method with `@_unsafeInheritExecutor` otherwise we will break older 88 | // Swift versions since the semantics changed. 89 | @_disfavoredOverload 90 | @_unsafeInheritExecutor 91 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 92 | public func withGracefulShutdownHandler( 93 | operation: () async throws -> T, 94 | onGracefulShutdown handler: @Sendable @escaping () -> Void 95 | ) async rethrows -> T { 96 | guard let gracefulShutdownManager = TaskLocals.gracefulShutdownManager else { 97 | return try await operation() 98 | } 99 | 100 | // We have to keep track of our handler here to remove it once the operation is finished. 101 | let handlerID = gracefulShutdownManager.registerHandler(handler) 102 | defer { 103 | if let handlerID = handlerID { 104 | gracefulShutdownManager.removeHandler(handlerID) 105 | } 106 | } 107 | 108 | return try await operation() 109 | } 110 | 111 | #endif 112 | 113 | #if compiler(>=6.0) 114 | /// Execute an operation with a graceful shutdown or task cancellation handler that’s immediately invoked if the current task is 115 | /// shutting down gracefully or has been cancelled. 116 | /// 117 | /// This doesn’t check for graceful shutdown, and always executes the passed operation. 118 | /// The operation executes on the calling execution context and does not suspend by itself, unless the code contained within the closure does. 119 | /// If graceful shutdown or task cancellation occurs while the operation is running, the cancellation/graceful shutdown handler executes 120 | /// concurrently with the operation. 121 | /// 122 | /// When `withTaskCancellationOrGracefulShutdownHandler` is used in a Task that has already been gracefully shutdown or cancelled, the 123 | /// `onCancelOrGracefulShutdown` handler is executed immediately before operation gets to execute. This allows the `onCancelOrGracefulShutdown` 124 | /// handler to set some external “shutdown” flag that the operation may be atomically checking for in order to avoid performing any actual work 125 | /// once the operation gets to run. 126 | /// 127 | /// - Important: This method will only set up a graceful shutdown handler if run inside ``ServiceGroup`` otherwise no graceful shutdown handler 128 | /// will be set up. 129 | /// 130 | /// - Parameters: 131 | /// - isolation: The isolation of the method. Defaults to the isolation of the caller. 132 | /// - operation: The actual operation. 133 | /// - handler: The handler which is invoked once graceful shutdown or task cancellation has been triggered. 134 | // Unsafely inheriting the executor is safe to do here since we are not calling any other async method 135 | // except the operation. This makes sure no other executor hops would occur here. 136 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 137 | public func withTaskCancellationOrGracefulShutdownHandler( 138 | isolation: isolated (any Actor)? = #isolation, 139 | operation: () async throws -> T, 140 | onCancelOrGracefulShutdown handler: @Sendable @escaping () -> Void 141 | ) async rethrows -> T { 142 | return try await withTaskCancellationHandler { 143 | try await withGracefulShutdownHandler(isolation: isolation, operation: operation, onGracefulShutdown: handler) 144 | } onCancel: { 145 | handler() 146 | } 147 | } 148 | 149 | /// Execute an operation with a graceful shutdown or task cancellation handler that’s immediately invoked if the current task is 150 | /// shutting down gracefully or has been cancelled. 151 | /// 152 | /// Use ``withTaskCancellationOrGracefulShutdownHandler(isolation:operation:onCancelOrGracefulShutdown:)`` instead. 153 | @available(*, deprecated, message: "Use the method with the isolation parameter instead.") 154 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 155 | @_disfavoredOverload 156 | public func withTaskCancellationOrGracefulShutdownHandler( 157 | operation: () async throws -> T, 158 | onCancelOrGracefulShutdown handler: @Sendable @escaping () -> Void 159 | ) async rethrows -> T { 160 | return try await withTaskCancellationHandler { 161 | try await withGracefulShutdownHandler(operation: operation, onGracefulShutdown: handler) 162 | } onCancel: { 163 | handler() 164 | } 165 | } 166 | #else 167 | /// Execute an operation with a graceful shutdown or task cancellation handler that’s immediately invoked if the current task is 168 | /// shutting down gracefully or has been cancelled. 169 | /// 170 | /// Use ``withTaskCancellationOrGracefulShutdownHandler(isolation:operation:onCancelOrGracefulShutdown:)`` instead. 171 | @available(*, deprecated, message: "Use the method with the isolation parameter instead.") 172 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 173 | @_disfavoredOverload 174 | @_unsafeInheritExecutor 175 | public func withTaskCancellationOrGracefulShutdownHandler( 176 | operation: () async throws -> T, 177 | onCancelOrGracefulShutdown handler: @Sendable @escaping () -> Void 178 | ) async rethrows -> T { 179 | return try await withTaskCancellationHandler { 180 | try await withGracefulShutdownHandler(operation: operation, onGracefulShutdown: handler) 181 | } onCancel: { 182 | handler() 183 | } 184 | } 185 | #endif 186 | 187 | /// Waits until graceful shutdown is triggered. 188 | /// 189 | /// This method suspends the caller until graceful shutdown is triggered. If the calling task is cancelled before 190 | /// graceful shutdown is triggered then this method throws a `CancellationError`. 191 | /// 192 | /// - Throws: `CancellationError` if the task is cancelled. 193 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 194 | public func gracefulShutdown() async throws { 195 | switch await AsyncGracefulShutdownSequence().first(where: { _ in true }) { 196 | case .cancelled: 197 | throw CancellationError() 198 | case .gracefulShutdown: 199 | return 200 | case .none: 201 | fatalError() 202 | } 203 | } 204 | 205 | /// This is just a helper type for the result of our task group. 206 | enum ValueOrGracefulShutdown: Sendable { 207 | case value(T) 208 | case gracefulShutdown 209 | case cancelled 210 | } 211 | 212 | /// Cancels the closure when a graceful shutdown is triggered. 213 | /// 214 | /// - Parameter operation: The operation to cancel when a graceful shutdown is triggered. 215 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 216 | public func cancelWhenGracefulShutdown( 217 | _ operation: @Sendable @escaping () async throws -> T 218 | ) async rethrows -> T { 219 | return try await withThrowingTaskGroup(of: ValueOrGracefulShutdown.self) { group in 220 | group.addTask { 221 | let value = try await operation() 222 | return .value(value) 223 | } 224 | 225 | group.addTask { 226 | switch await CancellationWaiter().wait() { 227 | case .cancelled: 228 | return .cancelled 229 | case .gracefulShutdown: 230 | return .gracefulShutdown 231 | } 232 | } 233 | 234 | let result = try await group.next() 235 | group.cancelAll() 236 | 237 | switch result { 238 | case .value(let t): 239 | return t 240 | 241 | case .gracefulShutdown, .cancelled: 242 | switch try await group.next() { 243 | case .value(let t): 244 | return t 245 | case .gracefulShutdown, .cancelled: 246 | fatalError("Unexpectedly got gracefulShutdown from group.next()") 247 | case nil: 248 | fatalError("Unexpectedly got nil from group.next()") 249 | } 250 | 251 | case nil: 252 | fatalError("Unexpectedly got nil from group.next()") 253 | } 254 | } 255 | } 256 | 257 | /// Cancels the closure when a graceful shutdown was triggered. 258 | /// 259 | /// Use ``cancelWhenGracefulShutdown(_:)`` instead. 260 | /// - Parameter operation: The actual operation. 261 | #if compiler(>=6.0) 262 | @available(*, deprecated, renamed: "cancelWhenGracefulShutdown") 263 | #else 264 | // renamed pattern has been shown to cause compiler crashes in 5.x compilers. 265 | @available(*, deprecated, message: "renamed to cancelWhenGracefulShutdown") 266 | #endif 267 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 268 | public func cancelOnGracefulShutdown( 269 | _ operation: @Sendable @escaping () async throws -> T 270 | ) async rethrows -> T? { 271 | return try await cancelWhenGracefulShutdown(operation) 272 | } 273 | 274 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 275 | extension Task where Success == Never, Failure == Never { 276 | /// A Boolean value that indicates whether the task is gracefully shutting down 277 | /// 278 | /// After the value of this property becomes `true`, it remains `true` indefinitely. There is no way to undo a graceful shutdown. 279 | public static var isShuttingDownGracefully: Bool { 280 | guard let gracefulShutdownManager = TaskLocals.gracefulShutdownManager else { 281 | return false 282 | } 283 | 284 | return gracefulShutdownManager.isShuttingDown 285 | } 286 | } 287 | 288 | @_spi(TestKit) 289 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 290 | public enum TaskLocals { 291 | @TaskLocal 292 | @_spi(TestKit) 293 | public static var gracefulShutdownManager: GracefulShutdownManager? 294 | } 295 | 296 | @_spi(TestKit) 297 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 298 | public final class GracefulShutdownManager: @unchecked Sendable { 299 | struct Handler { 300 | /// The id of the handler. 301 | var id: UInt64 302 | /// The actual handler. 303 | var handler: () -> Void 304 | } 305 | 306 | struct State { 307 | /// The currently registered handlers. 308 | fileprivate var handlers = [Handler]() 309 | /// A counter to assign a unique number to each handler. 310 | fileprivate var handlerCounter: UInt64 = 0 311 | /// A boolean indicating if we have been shutdown already. 312 | fileprivate var isShuttingDown = false 313 | /// Continuations to resume after all of the handlers have been executed. 314 | fileprivate var gracefulShutdownFinishedContinuations = [CheckedContinuation]() 315 | } 316 | 317 | private let state = LockedValueBox(State()) 318 | 319 | var isShuttingDown: Bool { 320 | self.state.withLockedValue { return $0.isShuttingDown } 321 | } 322 | 323 | @_spi(TestKit) 324 | public init() {} 325 | 326 | func registerHandler(_ handler: @Sendable @escaping () -> Void) -> UInt64? { 327 | return self.state.withLockedValue { state in 328 | guard state.isShuttingDown else { 329 | defer { 330 | state.handlerCounter += 1 331 | } 332 | let handlerID = state.handlerCounter 333 | state.handlers.append(.init(id: handlerID, handler: handler)) 334 | 335 | return handlerID 336 | } 337 | // We are already shutting down so we just run the handler now. 338 | handler() 339 | return nil 340 | } 341 | } 342 | 343 | func removeHandler(_ handlerID: UInt64) { 344 | self.state.withLockedValue { state in 345 | guard let index = state.handlers.firstIndex(where: { $0.id == handlerID }) else { 346 | // This can happen because if shutdownGracefully ran while the operation was still in progress 347 | return 348 | } 349 | 350 | state.handlers.remove(at: index) 351 | } 352 | } 353 | 354 | @_spi(TestKit) 355 | public func shutdownGracefully() { 356 | self.state.withLockedValue { state in 357 | guard !state.isShuttingDown else { 358 | return 359 | } 360 | state.isShuttingDown = true 361 | 362 | for handler in state.handlers { 363 | handler.handler() 364 | } 365 | 366 | state.handlers.removeAll() 367 | 368 | for continuation in state.gracefulShutdownFinishedContinuations { 369 | continuation.resume() 370 | } 371 | 372 | state.gracefulShutdownFinishedContinuations.removeAll() 373 | } 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /Tests/ServiceLifecycleTests/ServiceGroupAddServiceTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Logging 16 | import ServiceLifecycle 17 | import UnixSignals 18 | import XCTest 19 | 20 | private struct ExampleError: Error, Hashable {} 21 | 22 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 23 | final class ServiceGroupAddServiceTests: XCTestCase { 24 | 25 | func testAddService_whenNotRunning() async { 26 | let mockService = MockService(description: "Service1") 27 | let serviceGroup = self.makeServiceGroup() 28 | await serviceGroup.addServiceUnlessShutdown(mockService) 29 | 30 | await withThrowingTaskGroup(of: Void.self) { group in 31 | group.addTask { 32 | try await serviceGroup.run() 33 | } 34 | 35 | var eventIterator = mockService.events.makeAsyncIterator() 36 | await XCTAsyncAssertEqual(await eventIterator.next(), .run) 37 | 38 | group.cancelAll() 39 | await XCTAsyncAssertEqual(await eventIterator.next(), .runCancelled) 40 | 41 | await mockService.resumeRunContinuation(with: .success(())) 42 | } 43 | 44 | } 45 | 46 | func testAddService_whenRunning() async throws { 47 | let mockService1 = MockService(description: "Service1") 48 | let mockService2 = MockService(description: "Service2") 49 | let serviceGroup = self.makeServiceGroup( 50 | services: [.init(service: mockService1)] 51 | ) 52 | 53 | await withThrowingTaskGroup(of: Void.self) { group in 54 | group.addTask { 55 | try await serviceGroup.run() 56 | } 57 | 58 | var eventIterator1 = mockService1.events.makeAsyncIterator() 59 | var eventIterator2 = mockService2.events.makeAsyncIterator() 60 | await XCTAsyncAssertEqual(await eventIterator1.next(), .run) 61 | 62 | await serviceGroup.addServiceUnlessShutdown(mockService2) 63 | await XCTAsyncAssertEqual(await eventIterator2.next(), .run) 64 | 65 | await mockService1.resumeRunContinuation(with: .success(())) 66 | 67 | await XCTAsyncAssertEqual(await eventIterator2.next(), .runCancelled) 68 | await mockService2.resumeRunContinuation(with: .success(())) 69 | } 70 | } 71 | 72 | func testAddService_whenShuttingDown() async throws { 73 | let mockService1 = MockService(description: "Service1") 74 | let mockService2 = MockService(description: "Service2") 75 | let serviceGroup = self.makeServiceGroup( 76 | services: [.init(service: mockService1)] 77 | ) 78 | 79 | await withThrowingTaskGroup(of: Void.self) { group in 80 | group.addTask { 81 | try await serviceGroup.run() 82 | } 83 | 84 | var eventIterator1 = mockService1.events.makeAsyncIterator() 85 | await XCTAsyncAssertEqual(await eventIterator1.next(), .run) 86 | 87 | await serviceGroup.triggerGracefulShutdown() 88 | await XCTAsyncAssertEqual(await eventIterator1.next(), .shutdownGracefully) 89 | 90 | await serviceGroup.addServiceUnlessShutdown(mockService2) 91 | 92 | await mockService1.resumeRunContinuation(with: .success(())) 93 | } 94 | 95 | await XCTAsyncAssertEqual(await mockService2.hasRun, false) 96 | } 97 | 98 | func testAddService_whenCancelling() async throws { 99 | let mockService1 = MockService(description: "Service1") 100 | let mockService2 = MockService(description: "Service2") 101 | let serviceGroup = self.makeServiceGroup( 102 | services: [.init(service: mockService1)] 103 | ) 104 | 105 | await withThrowingTaskGroup(of: Void.self) { group in 106 | group.addTask { 107 | try await serviceGroup.run() 108 | } 109 | 110 | var eventIterator1 = mockService1.events.makeAsyncIterator() 111 | await XCTAsyncAssertEqual(await eventIterator1.next(), .run) 112 | 113 | group.cancelAll() 114 | 115 | await XCTAsyncAssertEqual(await eventIterator1.next(), .runCancelled) 116 | await serviceGroup.addServiceUnlessShutdown(mockService2) 117 | 118 | await mockService1.resumeRunContinuation(with: .success(())) 119 | } 120 | 121 | await XCTAsyncAssertEqual(await mockService2.hasRun, false) 122 | } 123 | 124 | func testRun_whenAddedServiceExitsEarly_andIgnore() async throws { 125 | let service1 = MockService(description: "Service1") 126 | let service2 = MockService(description: "Service2") 127 | let serviceGroup = self.makeServiceGroup( 128 | services: [], 129 | gracefulShutdownSignals: [.sigalrm] 130 | ) 131 | 132 | await serviceGroup.addServiceUnlessShutdown(.init(service: service1, successTerminationBehavior: .ignore)) 133 | 134 | try await withThrowingTaskGroup(of: Void.self) { group in 135 | group.addTask { 136 | try await serviceGroup.run() 137 | } 138 | 139 | var eventIterator1 = service1.events.makeAsyncIterator() 140 | var eventIterator2 = service2.events.makeAsyncIterator() 141 | await XCTAsyncAssertEqual(await eventIterator1.next(), .run) 142 | 143 | await serviceGroup.addServiceUnlessShutdown(.init(service: service2, failureTerminationBehavior: .ignore)) 144 | await XCTAsyncAssertEqual(await eventIterator2.next(), .run) 145 | 146 | await service1.resumeRunContinuation(with: .success(())) 147 | 148 | service2.sendPing() 149 | await XCTAsyncAssertEqual(await eventIterator2.next(), .runPing) 150 | 151 | await service2.resumeRunContinuation(with: .failure(ExampleError())) 152 | 153 | try await XCTAsyncAssertNoThrow(await group.next()) 154 | } 155 | } 156 | 157 | func testRun_whenAddedServiceExitsEarly_andShutdownGracefully() async throws { 158 | let service1 = MockService(description: "Service1") 159 | let service2 = MockService(description: "Service2") 160 | let service3 = MockService(description: "Service3") 161 | let serviceGroup = self.makeServiceGroup( 162 | services: [ 163 | .init(service: service1) 164 | ] 165 | ) 166 | 167 | await withThrowingTaskGroup(of: Void.self) { group in 168 | group.addTask { 169 | try await serviceGroup.run() 170 | } 171 | 172 | var eventIterator1 = service1.events.makeAsyncIterator() 173 | await XCTAsyncAssertEqual(await eventIterator1.next(), .run) 174 | 175 | await serviceGroup.addServiceUnlessShutdown( 176 | .init(service: service2, successTerminationBehavior: .gracefullyShutdownGroup) 177 | ) 178 | await serviceGroup.addServiceUnlessShutdown(.init(service: service3)) 179 | 180 | var eventIterator2 = service2.events.makeAsyncIterator() 181 | await XCTAsyncAssertEqual(await eventIterator2.next(), .run) 182 | 183 | var eventIterator3 = service3.events.makeAsyncIterator() 184 | await XCTAsyncAssertEqual(await eventIterator3.next(), .run) 185 | 186 | await service2.resumeRunContinuation(with: .success(())) 187 | 188 | // The last service should receive the shutdown signal first 189 | await XCTAsyncAssertEqual(await eventIterator3.next(), .shutdownGracefully) 190 | 191 | // Waiting to see that the remaining two are still running 192 | service1.sendPing() 193 | service3.sendPing() 194 | await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) 195 | await XCTAsyncAssertEqual(await eventIterator3.next(), .runPing) 196 | 197 | // Let's exit from the last service 198 | await service3.resumeRunContinuation(with: .success(())) 199 | 200 | // Waiting to see that the remaining is still running 201 | await XCTAsyncAssertEqual(await eventIterator1.next(), .shutdownGracefully) 202 | service1.sendPing() 203 | await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) 204 | 205 | // Let's exit from the first service 206 | await service1.resumeRunContinuation(with: .success(())) 207 | } 208 | } 209 | 210 | func testRun_whenAddedServiceThrows() async throws { 211 | let service1 = MockService(description: "Service1") 212 | let service2 = MockService(description: "Service2") 213 | let serviceGroup = self.makeServiceGroup( 214 | services: [.init(service: service1)], 215 | gracefulShutdownSignals: [.sigalrm] 216 | ) 217 | 218 | try await withThrowingTaskGroup(of: Void.self) { group in 219 | group.addTask { 220 | try await serviceGroup.run() 221 | } 222 | 223 | var service1EventIterator = service1.events.makeAsyncIterator() 224 | var service2EventIterator = service2.events.makeAsyncIterator() 225 | await XCTAsyncAssertEqual(await service1EventIterator.next(), .run) 226 | await serviceGroup.addServiceUnlessShutdown(service2) 227 | 228 | await XCTAsyncAssertEqual(await service2EventIterator.next(), .run) 229 | 230 | // Throwing from service2 here and expect that service1 gets cancelled 231 | await service2.resumeRunContinuation(with: .failure(ExampleError())) 232 | 233 | await XCTAsyncAssertEqual(await service1EventIterator.next(), .runCancelled) 234 | await service1.resumeRunContinuation(with: .success(())) 235 | 236 | try await XCTAsyncAssertThrowsError(await group.next()) { 237 | XCTAssertTrue($0 is ExampleError) 238 | } 239 | } 240 | } 241 | 242 | #if !os(Windows) 243 | func testGracefulShutdownOrdering_withAddedServices() async throws { 244 | let service1 = MockService(description: "Service1") 245 | let service2 = MockService(description: "Service2") 246 | let service3 = MockService(description: "Service3") 247 | let serviceGroup = self.makeServiceGroup( 248 | services: [.init(service: service1)], 249 | gracefulShutdownSignals: [.sigalrm] 250 | ) 251 | 252 | await withThrowingTaskGroup(of: Void.self) { group in 253 | group.addTask { 254 | try await serviceGroup.run() 255 | } 256 | 257 | var eventIterator1 = service1.events.makeAsyncIterator() 258 | await XCTAsyncAssertEqual(await eventIterator1.next(), .run) 259 | 260 | await serviceGroup.addServiceUnlessShutdown(service2) 261 | await serviceGroup.addServiceUnlessShutdown(service3) 262 | 263 | var eventIterator2 = service2.events.makeAsyncIterator() 264 | await XCTAsyncAssertEqual(await eventIterator2.next(), .run) 265 | 266 | var eventIterator3 = service3.events.makeAsyncIterator() 267 | await XCTAsyncAssertEqual(await eventIterator3.next(), .run) 268 | 269 | let pid = getpid() 270 | kill(pid, UnixSignal.sigalrm.rawValue) // ignore-unacceptable-language 271 | 272 | // The last service should receive the shutdown signal first 273 | await XCTAsyncAssertEqual(await eventIterator3.next(), .shutdownGracefully) 274 | 275 | // Waiting to see that all three are still running 276 | service1.sendPing() 277 | service2.sendPing() 278 | service3.sendPing() 279 | await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) 280 | await XCTAsyncAssertEqual(await eventIterator2.next(), .runPing) 281 | await XCTAsyncAssertEqual(await eventIterator3.next(), .runPing) 282 | 283 | // Let's exit from the last service 284 | await service3.resumeRunContinuation(with: .success(())) 285 | 286 | // The middle service should now receive the signal 287 | await XCTAsyncAssertEqual(await eventIterator2.next(), .shutdownGracefully) 288 | 289 | // Waiting to see that the two remaining are still running 290 | service1.sendPing() 291 | service2.sendPing() 292 | await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) 293 | await XCTAsyncAssertEqual(await eventIterator2.next(), .runPing) 294 | 295 | // Let's exit from the middle service 296 | await service2.resumeRunContinuation(with: .success(())) 297 | 298 | // The first service should now receive the signal 299 | await XCTAsyncAssertEqual(await eventIterator1.next(), .shutdownGracefully) 300 | 301 | // Waiting to see that the one remaining are still running 302 | service1.sendPing() 303 | await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) 304 | 305 | // Let's exit from the first service 306 | await service1.resumeRunContinuation(with: .success(())) 307 | } 308 | } 309 | 310 | func testGracefulShutdownOrdering_whenAddedServiceExits() async throws { 311 | let service1 = MockService(description: "Service1") 312 | let service2 = MockService(description: "Service2") 313 | let service3 = MockService(description: "Service3") 314 | 315 | let serviceGroup = self.makeServiceGroup( 316 | services: [.init(service: service1)], 317 | gracefulShutdownSignals: [.sigalrm] 318 | ) 319 | 320 | await withThrowingTaskGroup(of: Void.self) { group in 321 | group.addTask { 322 | try await serviceGroup.run() 323 | } 324 | 325 | var eventIterator1 = service1.events.makeAsyncIterator() 326 | await XCTAsyncAssertEqual(await eventIterator1.next(), .run) 327 | 328 | await serviceGroup.addServiceUnlessShutdown(service2) 329 | await serviceGroup.addServiceUnlessShutdown(service3) 330 | 331 | var eventIterator2 = service2.events.makeAsyncIterator() 332 | await XCTAsyncAssertEqual(await eventIterator2.next(), .run) 333 | 334 | var eventIterator3 = service3.events.makeAsyncIterator() 335 | await XCTAsyncAssertEqual(await eventIterator3.next(), .run) 336 | 337 | let pid = getpid() 338 | kill(pid, UnixSignal.sigalrm.rawValue) // ignore-unacceptable-language 339 | 340 | // The last service should receive the shutdown signal first 341 | await XCTAsyncAssertEqual(await eventIterator3.next(), .shutdownGracefully) 342 | 343 | // Waiting to see that all three are still running 344 | service1.sendPing() 345 | service2.sendPing() 346 | service3.sendPing() 347 | await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) 348 | await XCTAsyncAssertEqual(await eventIterator2.next(), .runPing) 349 | await XCTAsyncAssertEqual(await eventIterator3.next(), .runPing) 350 | 351 | // Let's exit from the last service 352 | await service3.resumeRunContinuation(with: .success(())) 353 | 354 | // The middle service should now receive the signal 355 | await XCTAsyncAssertEqual(await eventIterator2.next(), .shutdownGracefully) 356 | 357 | // Waiting to see that the two remaining are still running 358 | service1.sendPing() 359 | service2.sendPing() 360 | await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) 361 | await XCTAsyncAssertEqual(await eventIterator2.next(), .runPing) 362 | 363 | // Let's exit from the first service 364 | await service1.resumeRunContinuation(with: .success(())) 365 | 366 | // The middle service should now receive a cancellation 367 | await XCTAsyncAssertEqual(await eventIterator2.next(), .runCancelled) 368 | 369 | // Let's exit from the first service 370 | await service2.resumeRunContinuation(with: .success(())) 371 | } 372 | } 373 | #endif 374 | 375 | // MARK: - Helpers 376 | 377 | private func makeServiceGroup( 378 | services: [ServiceGroupConfiguration.ServiceConfiguration] = [], 379 | gracefulShutdownSignals: [UnixSignal] = .init(), 380 | cancellationSignals: [UnixSignal] = .init(), 381 | maximumGracefulShutdownDuration: Duration? = nil, 382 | maximumCancellationDuration: Duration? = .seconds(5) 383 | ) -> ServiceGroup { 384 | var logger = Logger(label: "Tests") 385 | logger.logLevel = .debug 386 | 387 | var configuration = ServiceGroupConfiguration( 388 | services: services, 389 | gracefulShutdownSignals: gracefulShutdownSignals, 390 | cancellationSignals: cancellationSignals, 391 | logger: logger 392 | ) 393 | configuration.maximumGracefulShutdownDuration = maximumGracefulShutdownDuration 394 | configuration.maximumCancellationDuration = maximumCancellationDuration 395 | return .init( 396 | configuration: configuration 397 | ) 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /Tests/ServiceLifecycleTests/GracefulShutdownTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftServiceLifecycle open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the SwiftServiceLifecycle project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftServiceLifecycle project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ServiceLifecycle 16 | import ServiceLifecycleTestKit 17 | import XCTest 18 | 19 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 20 | final class GracefulShutdownTests: XCTestCase { 21 | func testWithGracefulShutdownHandler() async { 22 | var cont: AsyncStream.Continuation! 23 | let stream = AsyncStream { cont = $0 } 24 | let continuation = cont! 25 | 26 | await testGracefulShutdown { gracefulShutdownTestTrigger in 27 | await withGracefulShutdownHandler { 28 | await withTaskGroup(of: Void.self) { group in 29 | group.addTask { 30 | await stream.first { _ in true } 31 | } 32 | 33 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 34 | 35 | await group.waitForAll() 36 | } 37 | } onGracefulShutdown: { 38 | continuation.finish() 39 | } 40 | } 41 | } 42 | 43 | func testWithGracefulShutdownHandler_whenAlreadyShuttingDown() async { 44 | var cont: AsyncStream.Continuation! 45 | let stream = AsyncStream { cont = $0 } 46 | let continuation = cont! 47 | 48 | await testGracefulShutdown { gracefulShutdownTestTrigger in 49 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 50 | 51 | _ = await withGracefulShutdownHandler { 52 | continuation.yield("operation") 53 | } onGracefulShutdown: { 54 | continuation.yield("onGracefulShutdown") 55 | } 56 | } 57 | 58 | var iterator = stream.makeAsyncIterator() 59 | 60 | await XCTAsyncAssertEqual(await iterator.next(), "onGracefulShutdown") 61 | await XCTAsyncAssertEqual(await iterator.next(), "operation") 62 | } 63 | 64 | func testWithGracefulShutdownHandler_whenNested() async { 65 | var cont: AsyncStream.Continuation! 66 | let stream = AsyncStream { cont = $0 } 67 | let continuation = cont! 68 | 69 | await testGracefulShutdown { gracefulShutdownTestTrigger in 70 | await withGracefulShutdownHandler { 71 | continuation.yield("outerOperation") 72 | 73 | await withTaskGroup(of: Void.self) { group in 74 | group.addTask { 75 | await withGracefulShutdownHandler { 76 | continuation.yield("innerOperation") 77 | try? await Task.sleep(nanoseconds: 500_000_000) 78 | return () 79 | } onGracefulShutdown: { 80 | continuation.yield("innerOnGracefulShutdown") 81 | } 82 | } 83 | group.addTask { 84 | await withTaskGroup(of: Void.self) { group in 85 | group.addTask { 86 | await withGracefulShutdownHandler { 87 | continuation.yield("innerOperation") 88 | try? await Task.sleep(nanoseconds: 500_000_000) 89 | return () 90 | } onGracefulShutdown: { 91 | continuation.yield("innerOnGracefulShutdown") 92 | } 93 | } 94 | } 95 | } 96 | 97 | var iterator = stream.makeAsyncIterator() 98 | 99 | await XCTAsyncAssertEqual(await iterator.next(), "outerOperation") 100 | await XCTAsyncAssertEqual(await iterator.next(), "innerOperation") 101 | await XCTAsyncAssertEqual(await iterator.next(), "innerOperation") 102 | 103 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 104 | 105 | await XCTAsyncAssertEqual(await iterator.next(), "outerOnGracefulShutdown") 106 | await XCTAsyncAssertEqual(await iterator.next(), "innerOnGracefulShutdown") 107 | await XCTAsyncAssertEqual(await iterator.next(), "innerOnGracefulShutdown") 108 | } 109 | } onGracefulShutdown: { 110 | continuation.yield("outerOnGracefulShutdown") 111 | } 112 | } 113 | } 114 | 115 | func testWithGracefulShutdownHandler_cleansUpHandlerAfterScopeExit() async { 116 | final actor Foo { 117 | func run() async { 118 | await withGracefulShutdownHandler { 119 | } onGracefulShutdown: { 120 | self.foo() 121 | } 122 | } 123 | 124 | nonisolated func foo() {} 125 | } 126 | var foo: Foo! = Foo() 127 | weak var weakFoo: Foo? = foo 128 | 129 | await testGracefulShutdown { _ in 130 | await foo.run() 131 | 132 | XCTAssertNotNil(weakFoo) 133 | foo = nil 134 | XCTAssertNil(weakFoo) 135 | } 136 | } 137 | 138 | func testTaskShutdownGracefully() async { 139 | await testGracefulShutdown { gracefulShutdownTestTrigger in 140 | let task = Task { 141 | var cont: AsyncStream.Continuation! 142 | let stream = AsyncStream { cont = $0 } 143 | let continuation = cont! 144 | 145 | await withGracefulShutdownHandler { 146 | await withTaskGroup(of: Void.self) { group in 147 | group.addTask { 148 | await stream.first { _ in true } 149 | } 150 | 151 | await group.waitForAll() 152 | } 153 | } onGracefulShutdown: { 154 | continuation.finish() 155 | } 156 | } 157 | 158 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 159 | 160 | await task.value 161 | } 162 | } 163 | 164 | func testTaskGroupShutdownGracefully() async { 165 | await testGracefulShutdown { gracefulShutdownTestTrigger in 166 | await withTaskGroup(of: Void.self) { group in 167 | group.addTask { 168 | var cont: AsyncStream.Continuation! 169 | let stream = AsyncStream { cont = $0 } 170 | let continuation = cont! 171 | 172 | await withGracefulShutdownHandler { 173 | await withTaskGroup(of: Void.self) { group in 174 | group.addTask { 175 | await stream.first { _ in true } 176 | } 177 | 178 | await group.waitForAll() 179 | } 180 | } onGracefulShutdown: { 181 | continuation.finish() 182 | } 183 | } 184 | 185 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 186 | 187 | await group.waitForAll() 188 | } 189 | } 190 | } 191 | 192 | func testThrowingTaskGroupShutdownGracefully() async { 193 | await testGracefulShutdown { gracefulShutdownTestTrigger in 194 | await withThrowingTaskGroup(of: Void.self) { group in 195 | group.addTask { 196 | var cont: AsyncStream.Continuation! 197 | let stream = AsyncStream { cont = $0 } 198 | let continuation = cont! 199 | 200 | await withGracefulShutdownHandler { 201 | await withTaskGroup(of: Void.self) { group in 202 | group.addTask { 203 | await stream.first { _ in true } 204 | } 205 | 206 | await group.waitForAll() 207 | } 208 | } onGracefulShutdown: { 209 | continuation.finish() 210 | } 211 | } 212 | 213 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 214 | 215 | try! await group.waitForAll() 216 | } 217 | } 218 | } 219 | 220 | func testCancelWhenGracefulShutdown() async throws { 221 | try await testGracefulShutdown { gracefulShutdownTestTrigger in 222 | try await withThrowingTaskGroup(of: Void.self) { group in 223 | group.addTask { 224 | try await cancelWhenGracefulShutdown { 225 | try await Task.sleep(nanoseconds: 1_000_000_000_000) 226 | } 227 | } 228 | 229 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 230 | 231 | await XCTAsyncAssertThrowsError(try await group.next()) { error in 232 | XCTAssertTrue(error is CancellationError) 233 | } 234 | } 235 | } 236 | } 237 | 238 | func testResumesCancelWhenGracefulShutdownWithResult() async throws { 239 | await testGracefulShutdown { _ in 240 | let result = await cancelWhenGracefulShutdown { 241 | await Task.yield() 242 | return "hello" 243 | } 244 | 245 | XCTAssertEqual(result, "hello") 246 | } 247 | } 248 | 249 | func testIsShuttingDownGracefully() async throws { 250 | await testGracefulShutdown { gracefulShutdownTestTrigger in 251 | XCTAssertFalse(Task.isShuttingDownGracefully) 252 | 253 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 254 | 255 | XCTAssertTrue(Task.isShuttingDownGracefully) 256 | } 257 | } 258 | 259 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 260 | func testWaitForGracefulShutdown() async throws { 261 | try await testGracefulShutdown { gracefulShutdownTestTrigger in 262 | try await withThrowingTaskGroup(of: Void.self) { group in 263 | group.addTask { 264 | try await Task.sleep(for: .milliseconds(10)) 265 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 266 | } 267 | 268 | try await withGracefulShutdownHandler { 269 | try await gracefulShutdown() 270 | } onGracefulShutdown: { 271 | // No-op 272 | } 273 | 274 | try await group.waitForAll() 275 | } 276 | } 277 | } 278 | 279 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 280 | func testWaitForGracefulShutdown_WhenAlreadyShutdown() async throws { 281 | try await testGracefulShutdown { gracefulShutdownTestTrigger in 282 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 283 | 284 | try await withGracefulShutdownHandler { 285 | try await Task.sleep(for: .milliseconds(10)) 286 | try await gracefulShutdown() 287 | } onGracefulShutdown: { 288 | // No-op 289 | } 290 | } 291 | } 292 | 293 | func testWaitForGracefulShutdown_Cancellation() async throws { 294 | do { 295 | try await testGracefulShutdown { _ in 296 | try await withThrowingTaskGroup(of: Void.self) { group in 297 | group.addTask { 298 | try await gracefulShutdown() 299 | } 300 | 301 | group.cancelAll() 302 | try await group.waitForAll() 303 | } 304 | } 305 | XCTFail("Expected CancellationError to be thrown") 306 | } catch { 307 | XCTAssertTrue(error is CancellationError) 308 | } 309 | } 310 | 311 | func testWithTaskCancellationOrGracefulShutdownHandler_GracefulShutdown() async throws { 312 | var cont: AsyncStream.Continuation! 313 | let stream = AsyncStream { cont = $0 } 314 | let continuation = cont! 315 | 316 | await testGracefulShutdown { gracefulShutdownTestTrigger in 317 | await withTaskCancellationOrGracefulShutdownHandler { 318 | await withTaskGroup(of: Void.self) { group in 319 | group.addTask { 320 | await stream.first { _ in true } 321 | } 322 | 323 | gracefulShutdownTestTrigger.triggerGracefulShutdown() 324 | 325 | await group.waitForAll() 326 | } 327 | } onCancelOrGracefulShutdown: { 328 | continuation.finish() 329 | } 330 | } 331 | } 332 | 333 | func testWithTaskCancellationOrGracefulShutdownHandler_TaskCancellation() async throws { 334 | var cont: AsyncStream.Continuation! 335 | let stream = AsyncStream { cont = $0 } 336 | let continuation = cont! 337 | 338 | await withTaskGroup(of: Void.self) { group in 339 | group.addTask { 340 | var cancelCont: AsyncStream>.Continuation! 341 | let cancelStream = AsyncStream> { cancelCont = $0 } 342 | let cancelContinuation = cancelCont! 343 | await withTaskCancellationOrGracefulShutdownHandler { 344 | await withCheckedContinuation { (cont: CheckedContinuation) in 345 | cancelContinuation.yield(cont) 346 | continuation.finish() 347 | } 348 | } onCancelOrGracefulShutdown: { 349 | Task { 350 | let cont = await cancelStream.first(where: { _ in true })! 351 | cont.resume() 352 | } 353 | } 354 | } 355 | // wait for task to startup 356 | _ = await stream.first { _ in true } 357 | group.cancelAll() 358 | } 359 | } 360 | 361 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 362 | func testCancelWhenGracefulShutdownSurvivesCancellation() async throws { 363 | await withTaskGroup(of: Void.self) { group in 364 | group.addTask { 365 | await withGracefulShutdownHandler { 366 | await cancelWhenGracefulShutdown { 367 | await OnlyCancellationWaiter().cancellation 368 | 369 | try! await uncancellable { 370 | try! await Task.sleep(for: .milliseconds(500)) 371 | } 372 | } 373 | } onGracefulShutdown: { 374 | XCTFail("Unexpect graceful shutdown") 375 | } 376 | } 377 | 378 | group.cancelAll() 379 | } 380 | } 381 | 382 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 383 | func testCancelWhenGracefulShutdownSurvivesErrorThrown() async throws { 384 | struct MyError: Error, Equatable {} 385 | 386 | await withTaskGroup(of: Void.self) { group in 387 | group.addTask { 388 | do { 389 | try await withGracefulShutdownHandler { 390 | try await cancelWhenGracefulShutdown { 391 | await OnlyCancellationWaiter().cancellation 392 | 393 | try! await uncancellable { 394 | try! await Task.sleep(for: .milliseconds(500)) 395 | } 396 | 397 | throw MyError() 398 | } 399 | } onGracefulShutdown: { 400 | XCTFail("Unexpect graceful shutdown") 401 | } 402 | XCTFail("Expected to have thrown") 403 | } catch { 404 | XCTAssertEqual(error as? MyError, MyError()) 405 | } 406 | } 407 | 408 | group.cancelAll() 409 | } 410 | } 411 | } 412 | 413 | func uncancellable(_ closure: @escaping @Sendable () async throws -> Void) async throws { 414 | let task = Task { 415 | try await closure() 416 | } 417 | 418 | try await task.value 419 | } 420 | 421 | private actor OnlyCancellationWaiter { 422 | private var taskContinuation: CheckedContinuation? 423 | 424 | @usableFromInline 425 | init() {} 426 | 427 | @usableFromInline 428 | var cancellation: Void { 429 | get async { 430 | await withTaskCancellationHandler { 431 | await withCheckedContinuation { continuation in 432 | self.taskContinuation = continuation 433 | } 434 | } onCancel: { 435 | Task { 436 | await self.finish() 437 | } 438 | } 439 | } 440 | } 441 | 442 | private func finish() { 443 | self.taskContinuation?.resume() 444 | self.taskContinuation = nil 445 | } 446 | } 447 | --------------------------------------------------------------------------------