Skip to content

Commit faa9f17

Browse files
authored
expose the underlying lifecycle for composition reasons (#70)
motivation: application frameworks thae use ServiceLifecycle may need to expose their underlying lifecycle so they can be also composed into larger systems changes: * expose ServiceLifecycle::underlying * make sure backtrace installer is only called once per process * add swift-atomics dependency (>=5.2 and above) * add atomic helper (<5.2)
1 parent ac8ba89 commit faa9f17

File tree

9 files changed

+161
-14
lines changed

9 files changed

+161
-14
lines changed

Package.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,24 @@ let package = Package(
1414
.package(url: "https://github.com/swift-server/swift-backtrace.git", from: "1.1.1"),
1515
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), // used in tests
1616
],
17-
targets: [
18-
.target(name: "Lifecycle", dependencies: ["Logging", "Metrics", "Backtrace"]),
19-
.target(name: "LifecycleNIOCompat", dependencies: ["Lifecycle", "NIO"]),
20-
.testTarget(name: "LifecycleTests", dependencies: ["Lifecycle", "LifecycleNIOCompat"]),
21-
]
17+
targets: []
2218
)
19+
20+
#if compiler(>=5.2)
21+
package.dependencies += [
22+
.package(url: "https://github.com/apple/swift-atomics.git", .exact("0.0.3")), // exact since < 1.0
23+
]
24+
package.targets += [
25+
.target(name: "Lifecycle", dependencies: ["Logging", "Metrics", "Backtrace", "Atomics"]),
26+
]
27+
#else
28+
package.targets += [
29+
.target(name: "CLifecycleHelpers", dependencies: []),
30+
.target(name: "Lifecycle", dependencies: ["CLifecycleHelpers", "Logging", "Metrics", "Backtrace"]),
31+
]
32+
#endif
33+
34+
package.targets += [
35+
.target(name: "LifecycleNIOCompat", dependencies: ["Lifecycle", "NIO"]),
36+
.testTarget(name: "LifecycleTests", dependencies: ["Lifecycle", "LifecycleNIOCompat"]),
37+
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftServiceLifecycle open source project
4+
//
5+
// Copyright (c) 2021 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+
#include <CLifecycleAtomics.h>
16+
17+
#include <stdlib.h>
18+
#include <stdatomic.h>
19+
20+
struct c_lifecycle_atomic_bool *c_lifecycle_atomic_bool_create(bool value) {
21+
struct c_lifecycle_atomic_bool *wrapper = malloc(sizeof(*wrapper));
22+
atomic_init(&wrapper->value, value);
23+
return wrapper;
24+
}
25+
26+
bool c_lifecycle_atomic_bool_compare_and_exchange(struct c_lifecycle_atomic_bool *wrapper, bool expected, bool desired) {
27+
bool expected_copy = expected;
28+
return atomic_compare_exchange_strong(&wrapper->value, &expected_copy, desired);
29+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftServiceLifecycle open source project
4+
//
5+
// Copyright (c) 2021 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+
#include <stdbool.h>
16+
#include <stdint.h>
17+
18+
struct c_lifecycle_atomic_bool {
19+
_Atomic bool value;
20+
};
21+
struct c_lifecycle_atomic_bool * _Nonnull c_lifecycle_atomic_bool_create(bool value);
22+
23+
bool c_lifecycle_atomic_bool_compare_and_exchange(struct c_lifecycle_atomic_bool * _Nonnull atomic, bool expected, bool desired);

Sources/Lifecycle/Atomics.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftServiceLifecycle open source project
4+
//
5+
// Copyright (c) 2021 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(Atomics)
16+
import Atomics
17+
#else
18+
import CLifecycleHelpers
19+
#endif
20+
21+
internal class AtomicBoolean {
22+
#if canImport(Atomics)
23+
private let managed: ManagedAtomic<Bool>
24+
#else
25+
private let unmanaged: UnsafeMutablePointer<c_lifecycle_atomic_bool>
26+
#endif
27+
28+
init(_ value: Bool) {
29+
#if canImport(Atomics)
30+
self.managed = .init(value)
31+
#else
32+
self.unmanaged = c_lifecycle_atomic_bool_create(value)
33+
#endif
34+
}
35+
36+
deinit {
37+
#if !canImport(Atomics)
38+
self.unmanaged.deinitialize(count: 1)
39+
#endif
40+
}
41+
42+
func compareAndSwap(expected: Bool, desired: Bool) -> Bool {
43+
#if canImport(Atomics)
44+
return self.managed.compareExchange(expected: expected, desired: desired, ordering: .acquiring).exchanged
45+
#else
46+
return c_lifecycle_atomic_bool_compare_and_exchange(self.unmanaged, expected, desired)
47+
#endif
48+
}
49+
}

Sources/Lifecycle/Lifecycle.swift

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the SwiftServiceLifecycle open source project
44
//
5-
// Copyright (c) 2019-2020 Apple Inc. and the SwiftServiceLifecycle project authors
5+
// Copyright (c) 2019-2021 Apple Inc. and the SwiftServiceLifecycle project authors
66
// Licensed under Apache License v2.0
77
//
88
// See LICENSE.txt for license information
@@ -20,6 +20,7 @@ import Glibc
2020
import Backtrace
2121
import Dispatch
2222
import Logging
23+
import Metrics
2324

2425
// MARK: - LifecycleTask
2526

@@ -99,13 +100,15 @@ public struct LifecycleHandler {
99100
/// `ServiceLifecycle` provides a basic mechanism to cleanly startup and shutdown the application, freeing resources in order before exiting.
100101
/// By default, also install shutdown hooks based on `Signal` and backtraces.
101102
public struct ServiceLifecycle {
103+
private static let backtracesInstalled = AtomicBoolean(false)
104+
102105
private let configuration: Configuration
103106

104107
/// The underlying `ComponentLifecycle` instance
105108
///
106109
/// Designed for composition purposes, mainly for frameworks that need to offer both top-level start/stop functionality and composition into larger systems.
107110
/// In other words, should not be used outside the context of building an Application framework.
108-
private let underlying: ComponentLifecycle
111+
public let underlying: ComponentLifecycle
109112

110113
/// Creates a `ServiceLifecycle` instance.
111114
///
@@ -114,11 +117,8 @@ public struct ServiceLifecycle {
114117
public init(configuration: Configuration = .init()) {
115118
self.configuration = configuration
116119
self.underlying = ComponentLifecycle(label: self.configuration.label, logger: self.configuration.logger)
117-
// setup backtrace trap as soon as possible
118-
if configuration.installBacktrace {
119-
self.log("installing backtrace")
120-
Backtrace.install()
121-
}
120+
// setup backtraces as soon as possible, so if we crash during setup we get a backtrace
121+
self.installBacktrace()
122122
}
123123

124124
/// Starts the provided `LifecycleTask` array.
@@ -127,13 +127,19 @@ public struct ServiceLifecycle {
127127
/// - parameters:
128128
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
129129
public func start(_ callback: @escaping (Error?) -> Void) {
130+
guard self.underlying.idle else {
131+
preconditionFailure("already started")
132+
}
130133
self.setupShutdownHook()
131134
self.underlying.start(on: self.configuration.callbackQueue, callback)
132135
}
133136

134137
/// Starts the provided `LifecycleTask` array and waits (blocking) until a shutdown `Signal` is captured or `shutdown` is called on another thread.
135138
/// Startup is performed in the order of items provided.
136139
public func startAndWait() throws {
140+
guard self.underlying.idle else {
141+
preconditionFailure("already started")
142+
}
137143
self.setupShutdownHook()
138144
try self.underlying.startAndWait(on: self.configuration.callbackQueue)
139145
}
@@ -152,6 +158,13 @@ public struct ServiceLifecycle {
152158
self.underlying.wait()
153159
}
154160

161+
private func installBacktrace() {
162+
if self.configuration.installBacktrace, ServiceLifecycle.backtracesInstalled.compareAndSwap(expected: false, desired: true) {
163+
self.log("installing backtrace")
164+
Backtrace.install()
165+
}
166+
}
167+
155168
private func setupShutdownHook() {
156169
self.configuration.shutdownSignal?.forEach { signal in
157170
self.log("setting up shutdown hook on \(signal)")
@@ -364,6 +377,16 @@ public class ComponentLifecycle: LifecycleTask {
364377
self.shutdownGroup.wait()
365378
}
366379

380+
// MARK: - internal
381+
382+
internal var idle: Bool {
383+
if case .idle = self.state {
384+
return true
385+
} else {
386+
return false
387+
}
388+
}
389+
367390
// MARK: - private
368391

369392
private func _start(on queue: DispatchQueue, tasks: [LifecycleTask], callback: @escaping (Error?) -> Void) {

Tests/LifecycleTests/ServiceLifecycleTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ extension ServiceLifecycleTests {
3333
("testNesting", testNesting),
3434
("testNesting2", testNesting2),
3535
("testSignalDescription", testSignalDescription),
36+
("testBacktracesInstalledOnce", testBacktracesInstalledOnce),
3637
]
3738
}
3839
}

Tests/LifecycleTests/ServiceLifecycleTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
@testable import Lifecycle
1616
import LifecycleNIOCompat
17+
import Logging
1718
import XCTest
1819

1920
final class ServiceLifecycleTests: XCTestCase {
@@ -226,4 +227,10 @@ final class ServiceLifecycleTests: XCTestCase {
226227
XCTAssertEqual("\(ServiceLifecycle.Signal.INT)", "Signal(INT, rawValue: \(ServiceLifecycle.Signal.INT.rawValue))")
227228
XCTAssertEqual("\(ServiceLifecycle.Signal.ALRM)", "Signal(ALRM, rawValue: \(ServiceLifecycle.Signal.ALRM.rawValue))")
228229
}
230+
231+
func testBacktracesInstalledOnce() {
232+
let config = ServiceLifecycle.Configuration(installBacktrace: true)
233+
_ = ServiceLifecycle(configuration: config)
234+
_ = ServiceLifecycle(configuration: config)
235+
}
229236
}

docker/docker-compose.1804.50.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ services:
1313
image: swift-service-lifecycle:18.04-5.0
1414
environment:
1515
- SKIP_SIGNAL_TEST=true
16-
16+
1717
shell:
1818
image: swift-service-lifecycle:18.04-5.0

scripts/soundness.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
1919

2020
function replace_acceptable_years() {
2121
# this needs to replace all acceptable forms with 'YEARS'
22-
sed -e 's/2017-2018/YEARS/' -e 's/2019-2020/YEARS/' -e 's/2019/YEARS/' -e 's/2020/YEARS/'
22+
sed -e 's/2017-2018/YEARS/' -e 's/2019-2020/YEARS/' -e 's/2019-2021/YEARS/' -e 's/2019/YEARS/' -e 's/2020/YEARS/' -e 's/2021/YEARS/'
2323
}
2424

2525
printf "=> Checking for unacceptable language... "

0 commit comments

Comments
 (0)