Skip to content

Commit 55f45e3

Browse files
authored
Conform ServiceGroup to Service (#172)
1 parent cdd6040 commit 55f45e3

File tree

2 files changed

+77
-7
lines changed

2 files changed

+77
-7
lines changed

Sources/ServiceLifecycle/ServiceGroup.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Logging
1616
import UnixSignals
1717

1818
/// A ``ServiceGroup`` is responsible for running a number of services, setting up signal handling and signalling graceful shutdown to the services.
19-
public actor ServiceGroup: Sendable {
19+
public actor ServiceGroup: Sendable, Service {
2020
/// The internal state of the ``ServiceGroup``.
2121
private enum State {
2222
/// The initial state of the group.
@@ -104,6 +104,16 @@ public actor ServiceGroup: Sendable {
104104
self.maximumCancellationDuration = configuration._maximumCancellationDuration
105105
}
106106

107+
/// Runs all the services by spinning up a child task per service.
108+
/// Furthermore, this method sets up the correct signal handlers
109+
/// for graceful shutdown.
110+
// We normally don't use underscored attributes but we really want to use the method with
111+
// file and line whenever possible.
112+
@_disfavoredOverload
113+
public func run() async throws {
114+
try await self.run(file: #file, line: #line)
115+
}
116+
107117
/// Runs all the services by spinning up a child task per service.
108118
/// Furthermore, this method sets up the correct signal handlers
109119
/// for graceful shutdown.

Tests/ServiceLifecycleTests/ServiceGroupTests.swift

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,7 @@ final class ServiceGroupTests: XCTestCase {
264264
await service3.resumeRunContinuation(with: .success(()))
265265

266266
// Waiting to see that the remaining is still running
267-
service1.sendPing()
268-
await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing)
269-
270-
// The first service should now receive the signal
271267
await XCTAsyncAssertEqual(await eventIterator1.next(), .shutdownGracefully)
272-
273-
// Waiting to see that the one remaining are still running
274268
service1.sendPing()
275269
await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing)
276270

@@ -1472,6 +1466,72 @@ final class ServiceGroupTests: XCTestCase {
14721466
}
14731467
}
14741468

1469+
func testTriggerGracefulShutdown_whenNestedGroup() async throws {
1470+
let service1 = MockService(description: "Service1")
1471+
let service2 = MockService(description: "Service2")
1472+
let service3 = MockService(description: "Service3")
1473+
let innerServiceGroup = self.makeServiceGroup(
1474+
services: [.init(service: service1), .init(service: service2), .init(service: service3)]
1475+
)
1476+
1477+
var logger = Logger(label: "Tests")
1478+
logger.logLevel = .debug
1479+
1480+
let outerServiceGroup = ServiceGroup(
1481+
services: [innerServiceGroup],
1482+
logger: logger
1483+
)
1484+
1485+
await withThrowingTaskGroup(of: Void.self) { group in
1486+
group.addTask {
1487+
try await outerServiceGroup.run()
1488+
}
1489+
1490+
var eventIterator1 = service1.events.makeAsyncIterator()
1491+
await XCTAsyncAssertEqual(await eventIterator1.next(), .run)
1492+
1493+
var eventIterator2 = service2.events.makeAsyncIterator()
1494+
await XCTAsyncAssertEqual(await eventIterator2.next(), .run)
1495+
1496+
var eventIterator3 = service3.events.makeAsyncIterator()
1497+
await XCTAsyncAssertEqual(await eventIterator3.next(), .run)
1498+
1499+
await outerServiceGroup.triggerGracefulShutdown()
1500+
1501+
// The last service should receive the shutdown signal first
1502+
await XCTAsyncAssertEqual(await eventIterator3.next(), .shutdownGracefully)
1503+
1504+
// Waiting to see that all three are still running
1505+
service1.sendPing()
1506+
service2.sendPing()
1507+
service3.sendPing()
1508+
await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing)
1509+
await XCTAsyncAssertEqual(await eventIterator2.next(), .runPing)
1510+
await XCTAsyncAssertEqual(await eventIterator3.next(), .runPing)
1511+
1512+
// Let's exit from the last service
1513+
await service3.resumeRunContinuation(with: .success(()))
1514+
1515+
// The middle service should now receive the signal
1516+
await XCTAsyncAssertEqual(await eventIterator2.next(), .shutdownGracefully)
1517+
1518+
// Waiting to see that the two remaining are still running
1519+
service1.sendPing()
1520+
service2.sendPing()
1521+
await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing)
1522+
await XCTAsyncAssertEqual(await eventIterator2.next(), .runPing)
1523+
1524+
// Let's exit from the first service
1525+
await service1.resumeRunContinuation(with: .success(()))
1526+
1527+
// The middle service should now receive a cancellation
1528+
await XCTAsyncAssertEqual(await eventIterator2.next(), .runCancelled)
1529+
1530+
// Let's exit from the first service
1531+
await service2.resumeRunContinuation(with: .success(()))
1532+
}
1533+
}
1534+
14751535
// MARK: - Helpers
14761536

14771537
private func makeServiceGroup(

0 commit comments

Comments
 (0)