Skip to content

Commit 2744df5

Browse files
authored
unless shutdownIfNotStarted is set to true, dont shutdown items that failed to start (#107)
motivation: improve confusing behavior changes: * change startTask to return an array of started tasks instead of an index * only shutdown successfully started tasks or ones that have explicitly set shutdownIfNotStarted to true * add and adjust tests
1 parent c825a58 commit 2744df5

File tree

3 files changed

+172
-33
lines changed

3 files changed

+172
-33
lines changed

Sources/Lifecycle/Lifecycle.swift

+13-9
Original file line numberDiff line numberDiff line change
@@ -576,11 +576,13 @@ public class ComponentLifecycle: LifecycleTask {
576576
case .shuttingDown:
577577
self.stateLock.unlock()
578578
// shutdown was called while starting, or start failed, shutdown what we can
579-
let stoppable: [LifecycleTask]
580-
if started < tasks.count {
581-
stoppable = tasks.enumerated().filter { $0.offset <= started || $0.element.shutdownIfNotStarted }.map { $0.element }
582-
} else {
583-
stoppable = tasks
579+
var stoppable = started
580+
if started.count < tasks.count {
581+
let shutdownIfNotStarted = tasks.enumerated()
582+
.filter { $0.offset >= started.count }
583+
.map { $0.element }
584+
.filter { $0.shutdownIfNotStarted }
585+
stoppable.append(contentsOf: shutdownIfNotStarted)
584586
}
585587
self._shutdown(on: queue, tasks: stoppable) {
586588
callback(error)
@@ -596,25 +598,27 @@ public class ComponentLifecycle: LifecycleTask {
596598
}
597599
}
598600

599-
private func startTask(on queue: DispatchQueue, tasks: [LifecycleTask], index: Int, callback: @escaping (Int, Error?) -> Void) {
601+
private func startTask(on queue: DispatchQueue, tasks: [LifecycleTask], index: Int, callback: @escaping ([LifecycleTask], Error?) -> Void) {
600602
// async barrier
601603
let start = { (callback) -> Void in queue.async { tasks[index].start(callback) } }
602604
let callback = { (index, error) -> Void in queue.async { callback(index, error) } }
603605

604606
if index >= tasks.count {
605-
return callback(index, nil)
607+
return callback(tasks, nil)
606608
}
607609
self.logger.info("starting tasks [\(tasks[index].label)]")
608610
let startTime = DispatchTime.now()
609611
start { error in
610612
Timer(label: "\(self.label).\(tasks[index].label).lifecycle.start").recordNanoseconds(DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds)
611613
if let error = error {
612614
self.logger.error("failed to start [\(tasks[index].label)]: \(error)")
613-
return callback(index, error)
615+
let started = Array(tasks.prefix(index))
616+
return callback(started, error)
614617
}
615618
// shutdown called while starting
616619
if case .shuttingDown = self.stateLock.withLock({ self.state }) {
617-
return callback(index, nil)
620+
let started = index < tasks.count ? Array(tasks.prefix(index + 1)) : tasks
621+
return callback(started, nil)
618622
}
619623
self.startTask(on: queue, tasks: tasks, index: index + 1, callback: callback)
620624
}

Tests/LifecycleTests/ComponentLifecycleTests+XCTest.swift

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ extension ComponentLifecycleTests {
5959
("testNOOPHandlers", testNOOPHandlers),
6060
("testShutdownOnlyStarted", testShutdownOnlyStarted),
6161
("testShutdownWhenStartFailedIfAsked", testShutdownWhenStartFailedIfAsked),
62+
("testShutdownWhenStartFailsAndAsked", testShutdownWhenStartFailsAndAsked),
6263
("testStatefulSync", testStatefulSync),
6364
("testStatefulSyncStartError", testStatefulSyncStartError),
6465
("testStatefulSyncShutdownError", testStatefulSyncShutdownError),
@@ -71,6 +72,7 @@ extension ComponentLifecycleTests {
7172
("testAsyncAwait", testAsyncAwait),
7273
("testAsyncAwaitStateful", testAsyncAwaitStateful),
7374
("testAsyncAwaitErrorOnStart", testAsyncAwaitErrorOnStart),
75+
("testAsyncAwaitErrorOnStartShutdownRequested", testAsyncAwaitErrorOnStartShutdownRequested),
7476
("testAsyncAwaitErrorOnShutdown", testAsyncAwaitErrorOnShutdown),
7577
("testMetrics", testMetrics),
7678
]

Tests/LifecycleTests/ComponentLifecycleTests.swift

+157-24
Original file line numberDiff line numberDiff line change
@@ -911,21 +911,21 @@ final class ComponentLifecycleTests: XCTestCase {
911911
func testShutdownOnlyStarted() {
912912
class Item {
913913
let label: String
914-
let sempahore: DispatchSemaphore
914+
let semaphore: DispatchSemaphore
915915
let failStart: Bool
916-
let exptectedState: State
916+
let expectedState: State
917917
var state = State.idle
918918

919919
deinit {
920-
XCTAssertEqual(self.state, self.exptectedState, "\"\(self.label)\" should be \(self.exptectedState)")
921-
self.sempahore.signal()
920+
XCTAssertEqual(self.state, self.expectedState, "\"\(self.label)\" should be \(self.expectedState)")
921+
self.semaphore.signal()
922922
}
923923

924-
init(label: String, failStart: Bool, exptectedState: State, sempahore: DispatchSemaphore) {
924+
init(label: String, failStart: Bool, expectedState: State, semaphore: DispatchSemaphore) {
925925
self.label = label
926926
self.failStart = failStart
927-
self.exptectedState = exptectedState
928-
self.sempahore = sempahore
927+
self.expectedState = expectedState
928+
self.semaphore = semaphore
929929
}
930930

931931
func start() throws {
@@ -951,11 +951,12 @@ final class ComponentLifecycleTests: XCTestCase {
951951
}
952952

953953
let count = Int.random(in: 10 ..< 20)
954-
let sempahore = DispatchSemaphore(value: count)
954+
let semaphore = DispatchSemaphore(value: count)
955955
let lifecycle = ServiceLifecycle(configuration: .init(shutdownSignal: nil))
956956

957957
for index in 0 ..< count {
958-
let item = Item(label: "\(index)", failStart: index == count / 2, exptectedState: index <= count / 2 ? .shutdown : .idle, sempahore: sempahore)
958+
let failStart = index == count / 2
959+
let item = Item(label: "\(index)", failStart: failStart, expectedState: failStart ? .error : index <= count / 2 ? .shutdown : .idle, semaphore: semaphore)
959960
lifecycle.register(label: item.label, start: .sync(item.start), shutdown: .sync(item.shutdown))
960961
}
961962

@@ -965,25 +966,29 @@ final class ComponentLifecycleTests: XCTestCase {
965966
}
966967
lifecycle.wait()
967968

968-
XCTAssertEqual(.success, sempahore.wait(timeout: .now() + 1))
969+
XCTAssertEqual(.success, semaphore.wait(timeout: .now() + 1))
969970
}
970971

971972
func testShutdownWhenStartFailedIfAsked() {
972973
class DestructionSensitive {
973974
let label: String
974975
let failStart: Bool
975-
let sempahore: DispatchSemaphore
976+
let semaphore: DispatchSemaphore
976977
var state = State.idle
977978

978979
deinit {
979-
XCTAssertEqual(self.state, .shutdown, "\"\(self.label)\" should be shutdown")
980-
self.sempahore.signal()
980+
if failStart {
981+
XCTAssertEqual(self.state, .error, "\"\(self.label)\" should be error")
982+
} else {
983+
XCTAssertEqual(self.state, .shutdown, "\"\(self.label)\" should be shutdown")
984+
}
985+
self.semaphore.signal()
981986
}
982987

983-
init(label: String, failStart: Bool = false, sempahore: DispatchSemaphore) {
988+
init(label: String, failStart: Bool = false, semaphore: DispatchSemaphore) {
984989
self.label = label
985990
self.failStart = failStart
986-
self.sempahore = sempahore
991+
self.semaphore = semaphore
987992
}
988993

989994
func start() throws {
@@ -1008,25 +1013,25 @@ final class ComponentLifecycleTests: XCTestCase {
10081013
struct InitError: Error {}
10091014
}
10101015

1011-
let sempahore = DispatchSemaphore(value: 6)
1016+
let semaphore = DispatchSemaphore(value: 6)
10121017
let lifecycle = ServiceLifecycle(configuration: .init(shutdownSignal: nil))
10131018

1014-
let item1 = DestructionSensitive(label: "1", sempahore: sempahore)
1019+
let item1 = DestructionSensitive(label: "1", semaphore: semaphore)
10151020
lifecycle.register(label: item1.label, start: .sync(item1.start), shutdown: .sync(item1.shutdown))
10161021

1017-
let item2 = DestructionSensitive(label: "2", sempahore: sempahore)
1022+
let item2 = DestructionSensitive(label: "2", semaphore: semaphore)
10181023
lifecycle.registerShutdown(label: item2.label, .sync(item2.shutdown))
10191024

1020-
let item3 = DestructionSensitive(label: "3", failStart: true, sempahore: sempahore)
1025+
let item3 = DestructionSensitive(label: "3", failStart: true, semaphore: semaphore)
10211026
lifecycle.register(label: item3.label, start: .sync(item3.start), shutdown: .sync(item3.shutdown))
10221027

1023-
let item4 = DestructionSensitive(label: "4", sempahore: sempahore)
1028+
let item4 = DestructionSensitive(label: "4", semaphore: semaphore)
10241029
lifecycle.registerShutdown(label: item4.label, .sync(item4.shutdown))
10251030

1026-
let item5 = DestructionSensitive(label: "5", sempahore: sempahore)
1031+
let item5 = DestructionSensitive(label: "5", semaphore: semaphore)
10271032
lifecycle.register(label: item5.label, start: .none, shutdown: .sync(item5.shutdown))
10281033

1029-
let item6 = DestructionSensitive(label: "6", sempahore: sempahore)
1034+
let item6 = DestructionSensitive(label: "6", semaphore: semaphore)
10301035
lifecycle.register(_LifecycleTask(label: item6.label, shutdownIfNotStarted: true, start: .sync(item6.start), shutdown: .sync(item6.shutdown)))
10311036

10321037
lifecycle.start { error in
@@ -1035,7 +1040,99 @@ final class ComponentLifecycleTests: XCTestCase {
10351040
}
10361041
lifecycle.wait()
10371042

1038-
XCTAssertEqual(.success, sempahore.wait(timeout: .now() + 1))
1043+
XCTAssertEqual(.success, semaphore.wait(timeout: .now() + 1))
1044+
}
1045+
1046+
func testShutdownWhenStartFailsAndAsked() {
1047+
class BadItem: LifecycleTask {
1048+
let label: String = UUID().uuidString
1049+
var shutdown: Bool = false
1050+
1051+
func start(_ callback: (Error?) -> Void) {
1052+
callback(TestError())
1053+
}
1054+
1055+
func shutdown(_ callback: (Error?) -> Void) {
1056+
self.shutdown = true
1057+
callback(nil)
1058+
}
1059+
}
1060+
1061+
do {
1062+
let lifecycle = ComponentLifecycle(label: "test")
1063+
1064+
let item = BadItem()
1065+
lifecycle.register(label: "test", start: .async(item.start), shutdown: .async(item.shutdown), shutdownIfNotStarted: true)
1066+
1067+
XCTAssertThrowsError(try lifecycle.startAndWait()) { error in
1068+
XCTAssert(error is TestError, "expected error to match")
1069+
}
1070+
1071+
XCTAssertTrue(item.shutdown, "expected item to be shutdown")
1072+
}
1073+
1074+
do {
1075+
let lifecycle = ComponentLifecycle(label: "test")
1076+
1077+
let item = BadItem()
1078+
lifecycle.register(label: "test", start: .async(item.start), shutdown: .async(item.shutdown), shutdownIfNotStarted: false)
1079+
1080+
XCTAssertThrowsError(try lifecycle.startAndWait()) { error in
1081+
XCTAssert(error is TestError, "expected error to match")
1082+
}
1083+
1084+
XCTAssertFalse(item.shutdown, "expected item to be not shutdown")
1085+
}
1086+
1087+
do {
1088+
let lifecycle = ComponentLifecycle(label: "test")
1089+
1090+
let item1 = GoodItem()
1091+
lifecycle.register(item1)
1092+
1093+
let item2 = BadItem()
1094+
lifecycle.register(label: "test", start: .async(item2.start), shutdown: .async(item2.shutdown), shutdownIfNotStarted: true)
1095+
1096+
let item3 = GoodItem()
1097+
lifecycle.register(item3)
1098+
1099+
let item4 = GoodItem()
1100+
lifecycle.registerShutdown(label: "test", .async(item4.shutdown))
1101+
1102+
XCTAssertThrowsError(try lifecycle.startAndWait()) { error in
1103+
XCTAssert(error is TestError, "expected error to match")
1104+
}
1105+
1106+
XCTAssertEqual(item1.state, .shutdown, "expected item to be shutdown")
1107+
XCTAssertTrue(item2.shutdown, "expected item to be shutdown")
1108+
XCTAssertEqual(item3.state, .idle, "expected item to be idle")
1109+
XCTAssertEqual(item4.state, .shutdown, "expected item to be shutdown")
1110+
}
1111+
1112+
do {
1113+
let lifecycle = ComponentLifecycle(label: "test")
1114+
1115+
let item1 = GoodItem()
1116+
lifecycle.register(item1)
1117+
1118+
let item2 = BadItem()
1119+
lifecycle.register(label: "test", start: .async(item2.start), shutdown: .async(item2.shutdown), shutdownIfNotStarted: false)
1120+
1121+
let item3 = GoodItem()
1122+
lifecycle.register(item3)
1123+
1124+
let item4 = GoodItem()
1125+
lifecycle.registerShutdown(label: "test", .async(item4.shutdown))
1126+
1127+
XCTAssertThrowsError(try lifecycle.startAndWait()) { error in
1128+
XCTAssert(error is TestError, "expected error to match")
1129+
}
1130+
1131+
XCTAssertEqual(item1.state, .shutdown, "expected item to be shutdown")
1132+
XCTAssertFalse(item2.shutdown, "expected item to be not shutdown")
1133+
XCTAssertEqual(item3.state, .idle, "expected item to be idle")
1134+
XCTAssertEqual(item4.state, .shutdown, "expected item to be shutdown")
1135+
}
10391136
}
10401137

10411138
func testStatefulSync() {
@@ -1398,7 +1495,43 @@ final class ComponentLifecycleTests: XCTestCase {
13981495
let lifecycle = ComponentLifecycle(label: "test")
13991496

14001497
let item = Item()
1401-
lifecycle.register(label: "test", start: .async(item.start), shutdown: .async(item.shutdown))
1498+
lifecycle.register(label: "test", start: .async(item.start), shutdown: .async(item.shutdown), shutdownIfNotStarted: false)
1499+
1500+
lifecycle.start { error in
1501+
XCTAssert(error is TestError, "expected error to match")
1502+
lifecycle.shutdown()
1503+
}
1504+
lifecycle.wait()
1505+
XCTAssertFalse(item.isShutdown, "expected item to be shutdown")
1506+
#endif
1507+
}
1508+
1509+
func testAsyncAwaitErrorOnStartShutdownRequested() throws {
1510+
#if compiler(<5.2)
1511+
return
1512+
#elseif compiler(<5.5)
1513+
throw XCTSkip()
1514+
#else
1515+
guard #available(macOS 12.0, *) else {
1516+
throw XCTSkip()
1517+
}
1518+
1519+
class Item {
1520+
var isShutdown: Bool = false
1521+
1522+
func start() async throws {
1523+
throw TestError()
1524+
}
1525+
1526+
func shutdown() async throws {
1527+
self.isShutdown = true // not thread safe but okay for this purpose
1528+
}
1529+
}
1530+
1531+
let lifecycle = ComponentLifecycle(label: "test")
1532+
1533+
let item = Item()
1534+
lifecycle.register(label: "test", start: .async(item.start), shutdown: .async(item.shutdown), shutdownIfNotStarted: true)
14021535

14031536
lifecycle.start { error in
14041537
XCTAssert(error is TestError, "expected error to match")

0 commit comments

Comments
 (0)