Skip to content

Commit ffe82de

Browse files
authored
replace semaphores with locks (#18)
motivation: Semaphores are not locks, if they are released too often by accident, we will get into deep trouble because there's no longer mutual exclusion. changes: use posix mutex
1 parent c62901a commit ffe82de

File tree

3 files changed

+127
-35
lines changed

3 files changed

+127
-35
lines changed

Sources/ServiceLauncher/Lifecycle.swift

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ public class Lifecycle {
2727
private let shutdownGroup = DispatchGroup()
2828

2929
private var state = State.idle
30-
private let stateSemaphore = DispatchSemaphore(value: 1)
30+
private let stateLock = Lock()
3131

3232
private var items = [LifecycleItem]()
33-
private let itemsSemaphore = DispatchSemaphore(value: 1)
33+
private let itemsLock = Lock()
3434

3535
/// Creates a `Lifecycle` instance.
3636
public init() {
@@ -43,16 +43,23 @@ public class Lifecycle {
4343
/// - parameters:
4444
/// - configuration: Defines lifecycle `Configuration`
4545
public func startAndWait(configuration: Configuration = .init()) throws {
46-
let waitSemaphore = DispatchSemaphore(value: 0)
46+
let waitLock = Lock()
47+
let group = DispatchGroup()
4748
var startError: Error?
48-
let items = self.itemsSemaphore.lock { self.items }
49+
let items = self.itemsLock.withLock { self.items }
50+
group.enter()
4951
self._start(configuration: configuration, items: items) { error in
50-
startError = error
51-
waitSemaphore.signal()
52+
waitLock.withLock {
53+
startError = error
54+
}
55+
group.leave()
5256
}
53-
waitSemaphore.wait()
54-
try startError.map { throw $0 }
5557
self.shutdownGroup.wait()
58+
try waitLock.withLock {
59+
startError
60+
}.map { error in
61+
throw error
62+
}
5663
}
5764

5865
/// Starts the provided `LifecycleItem` array.
@@ -62,27 +69,27 @@ public class Lifecycle {
6269
/// - configuration: Defines lifecycle `Configuration`
6370
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
6471
public func start(configuration: Configuration = .init(), callback: @escaping (Error?) -> Void) {
65-
let items = self.itemsSemaphore.lock { self.items }
72+
let items = self.itemsLock.withLock { self.items }
6673
self._start(configuration: configuration, items: items, callback: callback)
6774
}
6875

6976
/// Shuts down the `LifecycleItem` array provided in `start` or `startAndWait`.
7077
/// Shutdown is performed in reverse order of items provided.
7178
public func shutdown(on queue: DispatchQueue = DispatchQueue.global()) {
72-
self.stateSemaphore.wait()
79+
self.stateLock.lock()
7380
switch self.state {
7481
case .idle:
75-
self.stateSemaphore.signal()
82+
self.stateLock.unlock()
7683
self.shutdownGroup.leave()
7784
case .starting:
7885
self.state = .shuttingDown
79-
self.stateSemaphore.signal()
86+
self.stateLock.unlock()
8087
case .shuttingDown, .shutdown:
81-
self.stateSemaphore.signal()
88+
self.stateLock.unlock()
8289
return
8390
case .started(let items):
8491
self.state = .shuttingDown
85-
self.stateSemaphore.signal()
92+
self.stateLock.unlock()
8693
self._shutdown(on: queue, items: items) {
8794
self.shutdownGroup.leave()
8895
}
@@ -97,7 +104,7 @@ public class Lifecycle {
97104
// MARK: - private
98105

99106
private func _start(configuration: Configuration, items: [LifecycleItem], callback: @escaping (Error?) -> Void) {
100-
self.stateSemaphore.lock {
107+
self.stateLock.withLock {
101108
guard case .idle = self.state else {
102109
preconditionFailure("invalid state, \(self.state)")
103110
}
@@ -110,21 +117,21 @@ public class Lifecycle {
110117
self.state = .starting
111118
}
112119
self._start(on: configuration.callbackQueue, items: items, index: 0) { _, error in
113-
self.stateSemaphore.wait()
120+
self.stateLock.lock()
114121
if error != nil {
115122
self.state = .shuttingDown
116123
}
117124
switch self.state {
118125
case .shuttingDown:
119-
self.stateSemaphore.signal()
126+
self.stateLock.unlock()
120127
// shutdown was called while starting, or start failed, shutdown what we can
121128
self._shutdown(on: configuration.callbackQueue, items: items) {
122129
callback(error)
123130
self.shutdownGroup.leave()
124131
}
125132
case .starting:
126133
self.state = .started(items)
127-
self.stateSemaphore.signal()
134+
self.stateLock.unlock()
128135
configuration.shutdownSignal?.forEach { signal in
129136
self.logger.info("setting up shutdown hook on \(signal)")
130137
let signalSource = Lifecycle.trap(signal: signal, handler: { signal in
@@ -157,23 +164,20 @@ public class Lifecycle {
157164
return callback(index, error)
158165
}
159166
// shutdown called while starting
160-
self.stateSemaphore.wait()
161-
if case .shuttingDown = self.state {
162-
self.stateSemaphore.signal()
167+
if case .shuttingDown = self.stateLock.withLock({ self.state }) {
163168
return callback(index, nil)
164169
}
165-
self.stateSemaphore.signal()
166170
self._start(on: queue, items: items, index: index + 1, callback: callback)
167171
}
168172
}
169173

170174
private func _shutdown(on queue: DispatchQueue, items: [LifecycleItem], callback: @escaping () -> Void) {
171-
self.stateSemaphore.lock {
175+
self.stateLock.withLock {
172176
self.logger.info("shutting down lifecycle")
173177
self.state = .shuttingDown
174178
}
175179
self._shutdown(on: queue, items: items.reversed(), index: 0) {
176-
self.stateSemaphore.lock {
180+
self.stateLock.withLock {
177181
guard case .shuttingDown = self.state else {
178182
preconditionFailure("invalid state, \(self.state)")
179183
}
@@ -253,12 +257,12 @@ public extension Lifecycle {
253257
/// - parameters:
254258
/// - items: one or more `LifecycleItem`.
255259
func register(_ items: [LifecycleItem]) {
256-
self.stateSemaphore.lock {
260+
self.stateLock.withLock {
257261
guard case .idle = self.state else {
258262
preconditionFailure("invalid state, \(self.state)")
259263
}
260264
}
261-
self.itemsSemaphore.lock {
265+
self.itemsLock.withLock {
262266
self.items.append(contentsOf: items)
263267
}
264268
}
@@ -394,11 +398,3 @@ extension Lifecycle {
394398
internal static let ALRM: Signal = Signal(rawValue: SIGALRM)
395399
}
396400
}
397-
398-
private extension DispatchSemaphore {
399-
func lock<T>(_ body: () -> T) -> T {
400-
self.wait()
401-
defer { self.signal() }
402-
return body()
403-
}
404-
}

Sources/ServiceLauncher/Locks.swift

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftServiceLauncher open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the SwiftServiceLauncher 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 SwiftServiceLauncher 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-2018 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 os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
30+
import Darwin
31+
#else
32+
import Glibc
33+
#endif
34+
35+
/// A threading lock based on `libpthread` instead of `libdispatch`.
36+
///
37+
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
38+
/// of lock is safe to use with `libpthread`-based threading models, such as the
39+
/// one used by NIO.
40+
internal final class Lock {
41+
private let mutex: UnsafeMutablePointer<pthread_mutex_t> = UnsafeMutablePointer.allocate(capacity: 1)
42+
43+
/// Create a new lock.
44+
public init() {
45+
let err = pthread_mutex_init(self.mutex, nil)
46+
precondition(err == 0)
47+
}
48+
49+
deinit {
50+
let err = pthread_mutex_destroy(self.mutex)
51+
precondition(err == 0)
52+
self.mutex.deallocate()
53+
}
54+
55+
/// Acquire the lock.
56+
///
57+
/// Whenever possible, consider using `withLock` instead of this method and
58+
/// `unlock`, to simplify lock handling.
59+
public func lock() {
60+
let err = pthread_mutex_lock(self.mutex)
61+
precondition(err == 0)
62+
}
63+
64+
/// Release the lock.
65+
///
66+
/// Whenever possible, consider using `withLock` instead of this method and
67+
/// `lock`, to simplify lock handling.
68+
public func unlock() {
69+
let err = pthread_mutex_unlock(self.mutex)
70+
precondition(err == 0)
71+
}
72+
}
73+
74+
extension Lock {
75+
/// Acquire the lock for the duration of the given block.
76+
///
77+
/// This convenience method should be preferred to `lock` and `unlock` in
78+
/// most situations, as it ensures that the lock will be released regardless
79+
/// of how `body` exits.
80+
///
81+
/// - Parameter body: The block to execute while holding the lock.
82+
/// - Returns: The value returned by the block.
83+
@inlinable
84+
internal func withLock<T>(_ body: () throws -> T) rethrows -> T {
85+
self.lock()
86+
defer {
87+
self.unlock()
88+
}
89+
return try body()
90+
}
91+
92+
// specialise Void return (for performance)
93+
@inlinable
94+
internal func withLockVoid(_ body: () throws -> Void) rethrows {
95+
try self.withLock(body)
96+
}
97+
}

Tests/ServiceLauncherTests/LifecycleTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import NIO
16-
import NIOConcurrencyHelpers
1716
@testable import ServiceLauncher
1817
import ServiceLauncherNIOCompat
1918
import XCTest

0 commit comments

Comments
 (0)