Skip to content

Commit 66fb9c0

Browse files
authored
fix repeated shutdown issue on linux (#104)
motivation: repeatedly calling shutdown in linux was broken due to multiple calls to singal() which work on darwin but not linux changes: * conditionalize the call to signal to once per process * add test
1 parent b00bee0 commit 66fb9c0

File tree

3 files changed

+51
-3
lines changed

3 files changed

+51
-3
lines changed

Sources/Lifecycle/Lifecycle.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ public struct ServiceLifecycle {
333333
}
334334

335335
extension ServiceLifecycle {
336+
private static var trapped: Set<Int32> = []
337+
private static let trappedLock = Lock()
338+
336339
/// Setup a signal trap.
337340
///
338341
/// - parameters:
@@ -342,14 +345,20 @@ extension ServiceLifecycle {
342345
/// - cancelAfterTrap: Defaults to false, which means the signal handler can be run multiple times. If true, the DispatchSignalSource will be cancelled after being trapped once.
343346
/// - returns: a `DispatchSourceSignal` for the given trap. The source must be cancelled by the caller.
344347
public static func trap(signal sig: Signal, handler: @escaping (Signal) -> Void, on queue: DispatchQueue = .global(), cancelAfterTrap: Bool = false) -> DispatchSourceSignal {
348+
// on linux, we can call singal() once per process
349+
self.trappedLock.withLockVoid {
350+
if !trapped.contains(sig.rawValue) {
351+
signal(sig.rawValue, SIG_IGN)
352+
trapped.insert(sig.rawValue)
353+
}
354+
}
345355
let signalSource = DispatchSource.makeSignalSource(signal: sig.rawValue, queue: queue)
346-
signal(sig.rawValue, SIG_IGN)
347-
signalSource.setEventHandler(handler: {
356+
signalSource.setEventHandler {
348357
if cancelAfterTrap {
349358
signalSource.cancel()
350359
}
351360
handler(sig)
352-
})
361+
}
353362
signalSource.resume()
354363
return signalSource
355364
}

Tests/LifecycleTests/ServiceLifecycleTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ extension ServiceLifecycleTests {
3434
("testNesting2", testNesting2),
3535
("testSignalDescription", testSignalDescription),
3636
("testBacktracesInstalledOnce", testBacktracesInstalledOnce),
37+
("testRepeatShutdown", testRepeatShutdown),
3738
]
3839
}
3940
}

Tests/LifecycleTests/ServiceLifecycleTests.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,42 @@ final class ServiceLifecycleTests: XCTestCase {
233233
_ = ServiceLifecycle(configuration: config)
234234
_ = ServiceLifecycle(configuration: config)
235235
}
236+
237+
func testRepeatShutdown() {
238+
if ProcessInfo.processInfo.environment["SKIP_SIGNAL_TEST"].flatMap(Bool.init) ?? false {
239+
print("skipping testRepeatShutdown")
240+
return
241+
}
242+
243+
var count = 0
244+
245+
struct Service {
246+
static let signal = ServiceLifecycle.Signal.ALRM
247+
248+
let lifecycle: ServiceLifecycle
249+
250+
init() {
251+
self.lifecycle = ServiceLifecycle(configuration: .init(shutdownSignal: [Service.signal]))
252+
self.lifecycle.register(GoodItem())
253+
}
254+
}
255+
256+
func gracefulShutdown() {
257+
let service = Service()
258+
service.lifecycle.start { error in
259+
XCTAssertNil(error, "not expecting error")
260+
kill(getpid(), Service.signal.rawValue)
261+
}
262+
263+
service.lifecycle.wait()
264+
count = count + 1 // not thread safe but fine for this purpose
265+
}
266+
267+
let attempts = Int.random(in: 2 ..< 5)
268+
for _ in 0 ..< attempts {
269+
gracefulShutdown()
270+
}
271+
272+
XCTAssertEqual(attempts, count)
273+
}
236274
}

0 commit comments

Comments
 (0)