Skip to content

Commit 98a9396

Browse files
authored
Propagate service error when gracefully shutting down (#152)
# Motivation Propagate the service error when we shutdown the group after the service threw.
1 parent 1adb530 commit 98a9396

File tree

2 files changed

+73
-7
lines changed

2 files changed

+73
-7
lines changed

Sources/ServiceLifecycle/ServiceGroup.swift

+7-6
Original file line numberDiff line numberDiff line change
@@ -309,25 +309,25 @@ public actor ServiceGroup: Sendable {
309309
}
310310
}
311311

312-
case .serviceThrew(let service, let index, let error):
312+
case .serviceThrew(let service, let index, let serviceError):
313313
switch service.failureTerminationBehavior.behavior {
314314
case .cancelGroup:
315315
self.logger.debug(
316316
"Service threw error. Cancelling group.",
317317
metadata: [
318318
self.loggingConfiguration.keys.serviceKey: "\(service.service)",
319-
self.loggingConfiguration.keys.errorKey: "\(error)",
319+
self.loggingConfiguration.keys.errorKey: "\(serviceError)",
320320
]
321321
)
322322
group.cancelAll()
323-
return .failure(error)
323+
return .failure(serviceError)
324324

325325
case .gracefullyShutdownGroup:
326326
self.logger.debug(
327327
"Service threw error. Shutting down group.",
328328
metadata: [
329329
self.loggingConfiguration.keys.serviceKey: "\(service.service)",
330-
self.loggingConfiguration.keys.errorKey: "\(error)",
330+
self.loggingConfiguration.keys.errorKey: "\(serviceError)",
331331
]
332332
)
333333
services[index] = nil
@@ -338,16 +338,17 @@ public actor ServiceGroup: Sendable {
338338
group: &group,
339339
gracefulShutdownManagers: gracefulShutdownManagers
340340
)
341+
return .failure(serviceError)
341342
} catch {
342-
return .failure(error)
343+
return .failure(serviceError)
343344
}
344345

345346
case .ignore:
346347
self.logger.debug(
347348
"Service threw error.",
348349
metadata: [
349350
self.loggingConfiguration.keys.serviceKey: "\(service.service)",
350-
self.loggingConfiguration.keys.errorKey: "\(error)",
351+
self.loggingConfiguration.keys.errorKey: "\(serviceError)",
351352
]
352353
)
353354
services[index] = nil

Tests/ServiceLifecycleTests/ServiceGroupTests.swift

+66-1
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ final class ServiceGroupTests: XCTestCase {
433433
]
434434
)
435435

436-
await withThrowingTaskGroup(of: Void.self) { group in
436+
try await withThrowingTaskGroup(of: Void.self) { group in
437437
group.addTask {
438438
try await serviceGroup.run()
439439
}
@@ -474,6 +474,71 @@ final class ServiceGroupTests: XCTestCase {
474474

475475
// Let's exit from the first service
476476
await service1.resumeRunContinuation(with: .success(()))
477+
478+
try await XCTAsyncAssertThrowsError(await group.next()) {
479+
XCTAssertTrue($0 is ExampleError)
480+
}
481+
}
482+
}
483+
484+
func testRun_whenServiceThrows_andShutdownGracefully_andOtherServiceThrows() async throws {
485+
let service1 = MockService(description: "Service1")
486+
let service2 = MockService(description: "Service2")
487+
let service3 = MockService(description: "Service3")
488+
let serviceGroup = self.makeServiceGroup(
489+
services: [
490+
.init(service: service1),
491+
.init(service: service2, failureTerminationBehavior: .gracefullyShutdownGroup),
492+
.init(service: service3),
493+
]
494+
)
495+
496+
try await withThrowingTaskGroup(of: Void.self) { group in
497+
group.addTask {
498+
try await serviceGroup.run()
499+
}
500+
501+
var eventIterator1 = service1.events.makeAsyncIterator()
502+
await XCTAsyncAssertEqual(await eventIterator1.next(), .run)
503+
504+
var eventIterator2 = service2.events.makeAsyncIterator()
505+
await XCTAsyncAssertEqual(await eventIterator2.next(), .run)
506+
507+
var eventIterator3 = service3.events.makeAsyncIterator()
508+
await XCTAsyncAssertEqual(await eventIterator3.next(), .run)
509+
510+
await service2.resumeRunContinuation(with: .failure(ExampleError()))
511+
512+
// The last service should receive the shutdown signal first
513+
await XCTAsyncAssertEqual(await eventIterator3.next(), .shutdownGracefully)
514+
515+
// Waiting to see that all two are still running
516+
service1.sendPing()
517+
service3.sendPing()
518+
await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing)
519+
await XCTAsyncAssertEqual(await eventIterator3.next(), .runPing)
520+
521+
// Let's exit from the last service
522+
await service3.resumeRunContinuation(with: .success(()))
523+
524+
// Waiting to see that the remaining is still running
525+
service1.sendPing()
526+
await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing)
527+
528+
// The first service should now receive the signal
529+
await XCTAsyncAssertEqual(await eventIterator1.next(), .shutdownGracefully)
530+
531+
// Waiting to see that the one remaining are still running
532+
service1.sendPing()
533+
await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing)
534+
535+
// Let's throw from this service as well
536+
struct OtherError: Error {}
537+
await service1.resumeRunContinuation(with: .failure(OtherError()))
538+
539+
try await XCTAsyncAssertThrowsError(await group.next()) {
540+
XCTAssertTrue($0 is ExampleError)
541+
}
477542
}
478543
}
479544

0 commit comments

Comments
 (0)