Skip to content

Commit 22363fe

Browse files
authored
add the ability to de-register a task (#121)
motivation: sometimes a task needs to be de-registered since the task was manually shutdown outside the lifecycle scope changes: * registration APIs now return a registration key which can be used as a cancellation token * add API to de-register a task * refactor state to use a registery instead of array of tasks * add tests
1 parent 2133bfa commit 22363fe

File tree

4 files changed

+195
-36
lines changed

4 files changed

+195
-36
lines changed

Sources/Lifecycle/Lifecycle.swift

Lines changed: 125 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,13 @@ extension ServiceLifecycle {
417417
}
418418

419419
extension ServiceLifecycle: LifecycleTasksContainer {
420-
public func register(_ tasks: [LifecycleTask]) {
421-
self.underlying.register(tasks)
420+
@discardableResult
421+
public func register(_ tasks: [LifecycleTask]) -> [RegistrationKey] {
422+
return self.underlying.register(tasks)
423+
}
424+
425+
public func deregister(_ key: RegistrationKey) {
426+
self.underlying.deregister(key)
422427
}
423428
}
424429

@@ -462,7 +467,7 @@ public class ComponentLifecycle: LifecycleTask {
462467
fileprivate let logger: Logger
463468
fileprivate let shutdownGroup = DispatchGroup()
464469

465-
private var state = State.idle([])
470+
private var state = State.idle(Registry())
466471
private let stateLock = Lock()
467472

468473
/// Creates a `ComponentLifecycle` instance.
@@ -492,10 +497,10 @@ public class ComponentLifecycle: LifecycleTask {
492497
/// - on: `DispatchQueue` to run the handlers callback on
493498
/// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise.
494499
public func start(on queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
495-
guard case .idle(let tasks) = (self.stateLock.withLock { self.state }) else {
500+
guard case .idle(let registry) = (self.stateLock.withLock { self.state }) else {
496501
preconditionFailure("invalid state, \(self.state)")
497502
}
498-
self._start(on: queue, tasks: tasks, callback: callback)
503+
self._start(on: queue, registry: registry, callback: callback)
499504
}
500505

501506
/// Starts the provided `LifecycleTask` array and waits (blocking) until `shutdown` is called on another thread.
@@ -530,15 +535,15 @@ public class ComponentLifecycle: LifecycleTask {
530535

531536
self.stateLock.lock()
532537
switch self.state {
533-
case .idle(let tasks) where tasks.isEmpty:
538+
case .idle(let registry) where registry.isEmpty:
534539
self.state = .shutdown(nil)
535540
self.stateLock.unlock()
536541
defer { self.shutdownGroup.leave() }
537542
callback(nil)
538-
case .idle(let tasks):
543+
case .idle(let registry):
539544
self.stateLock.unlock()
540545
// attempt to shutdown any registered tasks
541-
let stoppable = tasks.filter { $0.shutdownIfNotStarted }
546+
let stoppable = registry.tasks.filter { $0.shutdownIfNotStarted }
542547
setupShutdownListener(.global())
543548
self._shutdown(on: .global(), tasks: stoppable, callback: self.shutdownGroup.leave)
544549
case .shutdown:
@@ -552,10 +557,10 @@ public class ComponentLifecycle: LifecycleTask {
552557
case .shuttingDown(let queue):
553558
self.stateLock.unlock()
554559
setupShutdownListener(queue)
555-
case .started(let queue, let tasks):
560+
case .started(let queue, let registry):
556561
self.stateLock.unlock()
557562
setupShutdownListener(queue)
558-
self._shutdown(on: queue, tasks: tasks, callback: self.shutdownGroup.leave)
563+
self._shutdown(on: queue, tasks: registry.tasks, callback: self.shutdownGroup.leave)
559564
}
560565
}
561566

@@ -576,7 +581,7 @@ public class ComponentLifecycle: LifecycleTask {
576581

577582
// MARK: - private
578583

579-
private func _start(on queue: DispatchQueue, tasks: [LifecycleTask], callback: @escaping (Error?) -> Void) {
584+
private func _start(on queue: DispatchQueue, registry: Registry, callback: @escaping (Error?) -> Void) {
580585
self.stateLock.withLock {
581586
guard case .idle = self.state else {
582587
preconditionFailure("invalid state, \(self.state)")
@@ -587,10 +592,10 @@ public class ComponentLifecycle: LifecycleTask {
587592
self.logger.info("starting")
588593
Counter(label: "\(self.label).lifecycle.start").increment()
589594

590-
if tasks.count == 0 {
595+
if registry.isEmpty {
591596
self.logger.notice("no tasks provided")
592597
}
593-
self.startTask(on: queue, tasks: tasks, index: 0) { started, error in
598+
self.startTask(on: queue, tasks: registry.tasks, index: 0) { started, error in
594599
self.stateLock.lock()
595600
if error != nil {
596601
self.state = .shuttingDown(queue)
@@ -600,8 +605,8 @@ public class ComponentLifecycle: LifecycleTask {
600605
self.stateLock.unlock()
601606
// shutdown was called while starting, or start failed, shutdown what we can
602607
var stoppable = started
603-
if started.count < tasks.count {
604-
let shutdownIfNotStarted = tasks.enumerated()
608+
if started.count < registry.tasks.count {
609+
let shutdownIfNotStarted = registry.tasks.enumerated()
605610
.filter { $0.offset >= started.count }
606611
.map { $0.element }
607612
.filter { $0.shutdownIfNotStarted }
@@ -612,7 +617,7 @@ public class ComponentLifecycle: LifecycleTask {
612617
self.shutdownGroup.leave()
613618
}
614619
case .starting:
615-
self.state = .started(queue, tasks)
620+
self.state = .started(queue, registry)
616621
self.stateLock.unlock()
617622
callback(nil)
618623
default:
@@ -697,70 +702,116 @@ public class ComponentLifecycle: LifecycleTask {
697702
}
698703

699704
private enum State {
700-
case idle([LifecycleTask])
705+
case idle(Registry)
701706
case starting(DispatchQueue)
702-
case started(DispatchQueue, [LifecycleTask])
707+
case started(DispatchQueue, Registry)
703708
case shuttingDown(DispatchQueue)
704709
case shutdown([String: Error]?)
705710
}
706711
}
707712

708713
extension ComponentLifecycle: LifecycleTasksContainer {
709-
public func register(_ tasks: [LifecycleTask]) {
714+
@discardableResult
715+
public func register(_ newTasks: [LifecycleTask]) -> [RegistrationKey] {
716+
let registrationKeys = self.stateLock.withLock { () -> [RegistrationKey] in
717+
guard case .idle(let registry) = self.state else {
718+
preconditionFailure("invalid state, \(self.state)")
719+
}
720+
return registry.add(newTasks)
721+
}
722+
return registrationKeys
723+
}
724+
725+
public func deregister(_ key: RegistrationKey) {
726+
func remove(key: RegistrationKey, tasks: [LifecycleTask], keys: [RegistrationKey]) -> ([LifecycleTask], [RegistrationKey]) {
727+
guard let index = keys.firstIndex(of: key) else {
728+
return (tasks, keys)
729+
}
730+
var updatedTasks = tasks
731+
updatedTasks.remove(at: index)
732+
var updatedKeys = keys
733+
updatedKeys.remove(at: index)
734+
return (updatedTasks, updatedKeys)
735+
}
736+
710737
self.stateLock.withLock {
711-
guard case .idle(let existing) = self.state else {
738+
switch self.state {
739+
case .idle(let registry), .started(_, let registry):
740+
registry.remove(key)
741+
default:
712742
preconditionFailure("invalid state, \(self.state)")
713743
}
714-
self.state = .idle(existing + tasks)
715744
}
716745
}
717746
}
718747

719748
/// A container of `LifecycleTask`, used to register additional `LifecycleTask`
720749
public protocol LifecycleTasksContainer {
721-
/// Adds a `LifecycleTask` to a `LifecycleTasks` collection.
750+
typealias RegistrationKey = String
751+
752+
/// Register a `LifecycleTask` with a `LifecycleTasksContainer`.
722753
///
723754
/// - parameters:
724755
/// - tasks: array of `LifecycleTask`.
725-
func register(_ tasks: [LifecycleTask])
756+
@discardableResult
757+
func register(_ tasks: [LifecycleTask]) -> [RegistrationKey]
758+
759+
/// De-register a `LifecycleTask` from a `LifecycleTasksContainer`.
760+
///
761+
/// - parameters:
762+
/// - registrationKey: The key returned by a register operation.
763+
func deregister(_ key: RegistrationKey)
726764
}
727765

728766
extension LifecycleTasksContainer {
729-
/// Adds a `LifecycleTask` to a `LifecycleTasks` collection.
767+
/// Register a `LifecycleTask` with a `LifecycleTasksContainer`.
768+
///
769+
/// - parameters:
770+
/// - tasks: one or more `LifecycleTask`.
771+
@discardableResult
772+
public func register(_ tasks: LifecycleTask ...) -> [RegistrationKey] {
773+
return self.register(tasks)
774+
}
775+
776+
/// Register a `LifecycleTask` with a `LifecycleTasksContainer`.
730777
///
731778
/// - parameters:
732779
/// - tasks: one or more `LifecycleTask`.
733-
public func register(_ tasks: LifecycleTask ...) {
734-
self.register(tasks)
780+
@discardableResult
781+
public func register(_ tasks: LifecycleTask) -> RegistrationKey {
782+
return self.register(tasks).first! // force the optional on the first in this case is safe
735783
}
736784

737-
/// Adds a `LifecycleTask` to a `LifecycleTasks` collection.
785+
/// Register a `LifecycleTask` with a `LifecycleTasksContainer`.
738786
///
739787
/// - parameters:
740788
/// - label: label of the item, useful for debugging.
741789
/// - start: `Handler` to perform the startup.
742790
/// - shutdown: `Handler` to perform the shutdown.
743-
public func register(label: String, start: LifecycleHandler, shutdown: LifecycleHandler, shutdownIfNotStarted: Bool? = nil) {
744-
self.register(_LifecycleTask(label: label, shutdownIfNotStarted: shutdownIfNotStarted, start: start, shutdown: shutdown))
791+
@discardableResult
792+
public func register(label: String, start: LifecycleHandler, shutdown: LifecycleHandler, shutdownIfNotStarted: Bool? = nil) -> RegistrationKey {
793+
return self.register(_LifecycleTask(label: label, shutdownIfNotStarted: shutdownIfNotStarted, start: start, shutdown: shutdown))
745794
}
746795

747-
/// Adds a `LifecycleTask` to a `LifecycleTasks` collection.
796+
/// Register a `LifecycleTask` with a `LifecycleTasksContainer`.
748797
///
749798
/// - parameters:
750799
/// - label: label of the item, useful for debugging.
751800
/// - handler: `Handler` to perform the shutdown.
752-
public func registerShutdown(label: String, _ handler: LifecycleHandler) {
753-
self.register(label: label, start: .none, shutdown: handler)
801+
@discardableResult
802+
public func registerShutdown(label: String, _ handler: LifecycleHandler) -> RegistrationKey {
803+
return self.register(label: label, start: .none, shutdown: handler)
754804
}
755805

756-
/// Add a stateful `LifecycleTask` to a `LifecycleTasks` collection.
806+
/// Register a stateful `LifecycleTask` with a `LifecycleTasksContainer`.
757807
///
758808
/// - parameters:
759809
/// - label: label of the item, useful for debugging.
760810
/// - start: `LifecycleStartHandler` to perform the startup and return the state.
761811
/// - shutdown: `LifecycleShutdownHandler` to perform the shutdown given the state.
762-
public func registerStateful<State>(label: String, start: LifecycleStartHandler<State>, shutdown: LifecycleShutdownHandler<State>) {
763-
self.register(StatefulLifecycleTask(label: label, start: start, shutdown: shutdown))
812+
@discardableResult
813+
public func registerStateful<State>(label: String, start: LifecycleStartHandler<State>, shutdown: LifecycleShutdownHandler<State>) -> RegistrationKey {
814+
return self.register(StatefulLifecycleTask(label: label, start: start, shutdown: shutdown))
764815
}
765816
}
766817

@@ -830,3 +881,42 @@ internal class StatefulLifecycleTask<State>: LifecycleTask {
830881

831882
struct UnknownState: Error {}
832883
}
884+
885+
private class Registry {
886+
typealias RegistrationKey = LifecycleTasksContainer.RegistrationKey
887+
888+
private var _tasks: [LifecycleTask] = []
889+
private var keys: [LifecycleTasksContainer.RegistrationKey] = []
890+
private let lock = Lock()
891+
892+
func add(_ tasks: [LifecycleTask]) -> [RegistrationKey] {
893+
// FIXME: better id generation scheme (cant use UUID)
894+
let keys: [RegistrationKey] = tasks.map { _ in
895+
let random = UInt64.random(in: UInt64.min ..< UInt64.max).addingReportingOverflow(DispatchTime.now().uptimeNanoseconds).partialValue
896+
return "task-\(random)"
897+
}
898+
self.lock.withLock {
899+
self._tasks.append(contentsOf: tasks)
900+
self.keys.append(contentsOf: keys)
901+
}
902+
return keys
903+
}
904+
905+
func remove(_ key: RegistrationKey) {
906+
self.lock.withLock {
907+
guard let index = self.keys.firstIndex(of: key) else {
908+
return
909+
}
910+
self._tasks.remove(at: index)
911+
self.keys.remove(at: index)
912+
}
913+
}
914+
915+
var tasks: [LifecycleTask] {
916+
return self.lock.withLock { self._tasks }
917+
}
918+
919+
var isEmpty: Bool {
920+
return self.lock.withLock { self._tasks.isEmpty }
921+
}
922+
}

Tests/LifecycleTests/ComponentLifecycleTests+XCTest.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ extension ComponentLifecycleTests {
2626
static var allTests: [(String, (ComponentLifecycleTests) -> () throws -> Void)] {
2727
return [
2828
("testStartThenShutdown", testStartThenShutdown),
29+
("testDeregister", testDeregister),
30+
("testDeregisterAfterStart", testDeregisterAfterStart),
2931
("testDefaultCallbackQueue", testDefaultCallbackQueue),
3032
("testUserDefinedCallbackQueue", testUserDefinedCallbackQueue),
3133
("testShutdownWhileStarting", testShutdownWhileStarting),

Tests/LifecycleTests/ComponentLifecycleTests.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,73 @@ final class ComponentLifecycleTests: XCTestCase {
3333
items.forEach { XCTAssertEqual($0.state, .shutdown, "expected item to be shutdown, but \($0.state)") }
3434
}
3535

36+
func testDeregister() {
37+
class BadItem: LifecycleTask {
38+
let label: String = UUID().uuidString
39+
40+
func start(_ callback: (Error?) -> Void) {
41+
callback(TestError())
42+
}
43+
44+
func shutdown(_ callback: (Error?) -> Void) {
45+
callback(TestError())
46+
}
47+
}
48+
49+
let lifecycle = ComponentLifecycle(label: "test")
50+
let itemToDeregister1 = BadItem()
51+
let itemToDeregister2 = BadItem()
52+
lifecycle.register(GoodItem())
53+
let key1 = lifecycle.register(itemToDeregister1)
54+
lifecycle.register(GoodItem())
55+
lifecycle.register(GoodItem())
56+
let key2 = lifecycle.register(itemToDeregister2)
57+
58+
lifecycle.deregister(key1)
59+
lifecycle.deregister(key2)
60+
61+
lifecycle.start { startError in
62+
XCTAssertNil(startError, "not expecting error")
63+
lifecycle.shutdown { shutdownErrors in
64+
XCTAssertNil(shutdownErrors, "not expecting error")
65+
}
66+
}
67+
lifecycle.wait()
68+
}
69+
70+
func testDeregisterAfterStart() {
71+
class BadItem: LifecycleTask {
72+
let label: String = UUID().uuidString
73+
74+
func start(_ callback: (Error?) -> Void) {
75+
callback(.none) // okay
76+
}
77+
78+
func shutdown(_ callback: (Error?) -> Void) {
79+
callback(TestError())
80+
}
81+
}
82+
83+
let lifecycle = ComponentLifecycle(label: "test")
84+
let itemToDeregister1 = BadItem()
85+
let itemToDeregister2 = BadItem()
86+
lifecycle.register(GoodItem())
87+
let key1 = lifecycle.register(itemToDeregister1)
88+
lifecycle.register(GoodItem())
89+
lifecycle.register(GoodItem())
90+
let key2 = lifecycle.register(itemToDeregister2)
91+
92+
lifecycle.start { startError in
93+
XCTAssertNil(startError, "not expecting error")
94+
lifecycle.deregister(key1)
95+
lifecycle.deregister(key2)
96+
lifecycle.shutdown { shutdownErrors in
97+
XCTAssertNil(shutdownErrors, "not expecting error")
98+
}
99+
}
100+
lifecycle.wait()
101+
}
102+
36103
func testDefaultCallbackQueue() throws {
37104
guard #available(OSX 10.12, *) else {
38105
return

docker/docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ services:
3434

3535
shell:
3636
<<: *common
37-
entrypoint: /bin/bash
37+
entrypoint: /bin/bash -l

0 commit comments

Comments
 (0)