Skip to content

Commit

Permalink
Conform ServiceGroup to Service (#172)
Browse files Browse the repository at this point in the history
  • Loading branch information
FranzBusch authored Feb 10, 2024
1 parent cdd6040 commit 55f45e3
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 7 deletions.
12 changes: 11 additions & 1 deletion Sources/ServiceLifecycle/ServiceGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Logging
import UnixSignals

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

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

/// Runs all the services by spinning up a child task per service.
/// Furthermore, this method sets up the correct signal handlers
/// for graceful shutdown.
Expand Down
72 changes: 66 additions & 6 deletions Tests/ServiceLifecycleTests/ServiceGroupTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,7 @@ final class ServiceGroupTests: XCTestCase {
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)

Expand Down Expand Up @@ -1472,6 +1466,72 @@ final class ServiceGroupTests: XCTestCase {
}
}

func testTriggerGracefulShutdown_whenNestedGroup() async throws {
let service1 = MockService(description: "Service1")
let service2 = MockService(description: "Service2")
let service3 = MockService(description: "Service3")
let innerServiceGroup = self.makeServiceGroup(
services: [.init(service: service1), .init(service: service2), .init(service: service3)]
)

var logger = Logger(label: "Tests")
logger.logLevel = .debug

let outerServiceGroup = ServiceGroup(
services: [innerServiceGroup],
logger: logger
)

await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await outerServiceGroup.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 outerServiceGroup.triggerGracefulShutdown()

// The last service should receive the shutdown signal first
await XCTAsyncAssertEqual(await eventIterator3.next(), .shutdownGracefully)

// Waiting to see that all three are still running
service1.sendPing()
service2.sendPing()
service3.sendPing()
await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing)
await XCTAsyncAssertEqual(await eventIterator2.next(), .runPing)
await XCTAsyncAssertEqual(await eventIterator3.next(), .runPing)

// Let's exit from the last service
await service3.resumeRunContinuation(with: .success(()))

// The middle service should now receive the signal
await XCTAsyncAssertEqual(await eventIterator2.next(), .shutdownGracefully)

// Waiting to see that the two remaining are still running
service1.sendPing()
service2.sendPing()
await XCTAsyncAssertEqual(await eventIterator1.next(), .runPing)
await XCTAsyncAssertEqual(await eventIterator2.next(), .runPing)

// Let's exit from the first service
await service1.resumeRunContinuation(with: .success(()))

// The middle service should now receive a cancellation
await XCTAsyncAssertEqual(await eventIterator2.next(), .runCancelled)

// Let's exit from the first service
await service2.resumeRunContinuation(with: .success(()))
}
}

// MARK: - Helpers

private func makeServiceGroup(
Expand Down

0 comments on commit 55f45e3

Please sign in to comment.