From 6e92f03be50aec6c9f77d93826a3828ac7514c7d Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 22 Aug 2023 20:08:34 +0100 Subject: [PATCH] Propagate service error when gracefully shutting down # Motivation Propagate the service error when we shutdown the group after the service threw. --- Sources/ServiceLifecycle/ServiceGroup.swift | 13 ++-- .../ServiceGroupTests.swift | 67 ++++++++++++++++++- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/Sources/ServiceLifecycle/ServiceGroup.swift b/Sources/ServiceLifecycle/ServiceGroup.swift index 67e3888..3673469 100644 --- a/Sources/ServiceLifecycle/ServiceGroup.swift +++ b/Sources/ServiceLifecycle/ServiceGroup.swift @@ -309,25 +309,25 @@ public actor ServiceGroup: Sendable { } } - case .serviceThrew(let service, let index, let error): + case .serviceThrew(let service, let index, let serviceError): switch service.failureTerminationBehavior.behavior { case .cancelGroup: self.logger.debug( "Service threw error. Cancelling group.", metadata: [ self.loggingConfiguration.keys.serviceKey: "\(service.service)", - self.loggingConfiguration.keys.errorKey: "\(error)", + self.loggingConfiguration.keys.errorKey: "\(serviceError)", ] ) group.cancelAll() - return .failure(error) + return .failure(serviceError) case .gracefullyShutdownGroup: self.logger.debug( "Service threw error. Shutting down group.", metadata: [ self.loggingConfiguration.keys.serviceKey: "\(service.service)", - self.loggingConfiguration.keys.errorKey: "\(error)", + self.loggingConfiguration.keys.errorKey: "\(serviceError)", ] ) services[index] = nil @@ -338,8 +338,9 @@ public actor ServiceGroup: Sendable { group: &group, gracefulShutdownManagers: gracefulShutdownManagers ) + return .failure(serviceError) } catch { - return .failure(error) + return .failure(serviceError) } case .ignore: @@ -347,7 +348,7 @@ public actor ServiceGroup: Sendable { "Service threw error.", metadata: [ self.loggingConfiguration.keys.serviceKey: "\(service.service)", - self.loggingConfiguration.keys.errorKey: "\(error)", + self.loggingConfiguration.keys.errorKey: "\(serviceError)", ] ) services[index] = nil diff --git a/Tests/ServiceLifecycleTests/ServiceGroupTests.swift b/Tests/ServiceLifecycleTests/ServiceGroupTests.swift index 855f4e3..0b8b7c9 100644 --- a/Tests/ServiceLifecycleTests/ServiceGroupTests.swift +++ b/Tests/ServiceLifecycleTests/ServiceGroupTests.swift @@ -433,7 +433,7 @@ final class ServiceGroupTests: XCTestCase { ] ) - await withThrowingTaskGroup(of: Void.self) { group in + try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { try await serviceGroup.run() } @@ -474,6 +474,71 @@ final class ServiceGroupTests: XCTestCase { // Let's exit from the first service await service1.resumeRunContinuation(with: .success(())) + + try await XCTAsyncAssertThrowsError(await group.next()) { + XCTAssertTrue($0 is ExampleError) + } + } + } + + func testRun_whenServiceThrows_andShutdownGracefully_andOtherServiceThrows() async throws { + let service1 = MockService(description: "Service1") + let service2 = MockService(description: "Service2") + let service3 = MockService(description: "Service3") + let serviceGroup = self.makeServiceGroup( + services: [ + .init(service: service1), + .init(service: service2, failureTerminationBehavior: .gracefullyShutdownGroup), + .init(service: service3), + ] + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await serviceGroup.run() + } + + var eventIterator1 = service1.events.makeAsyncIterator() + await XCTAsyncAssertEqual(await eventIterator1.next(), .run) + + var eventIterator2 = service2.events.makeAsyncIterator() + await XCTAsyncAssertEqual(await eventIterator2.next(), .run) + + var eventIterator3 = service3.events.makeAsyncIterator() + await XCTAsyncAssertEqual(await eventIterator3.next(), .run) + + await service2.resumeRunContinuation(with: .failure(ExampleError())) + + // The last service should receive the shutdown signal first + await XCTAsyncAssertEqual(await eventIterator3.next(), .shutdownGracefully) + + // Waiting to see that all two are still running + service1.sendPing() + service3.sendPing() + await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) + await XCTAsyncAssertEqual(await eventIterator3.next(), .runPing) + + // Let's exit from the last service + await service3.resumeRunContinuation(with: .success(())) + + // Waiting to see that the remaining is still running + service1.sendPing() + await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) + + // The first service should now receive the signal + await XCTAsyncAssertEqual(await eventIterator1.next(), .shutdownGracefully) + + // Waiting to see that the one remaining are still running + service1.sendPing() + await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing) + + // Let's throw from this service as well + struct OtherError: Error {} + await service1.resumeRunContinuation(with: .failure(OtherError())) + + try await XCTAsyncAssertThrowsError(await group.next()) { + XCTAssertTrue($0 is ExampleError) + } } }