Skip to content

Commit 91157ca

Browse files
author
Andrea Scuderi
committed
Fix Graceful Shutdown implementation
1 parent a97f69b commit 91157ca

File tree

4 files changed

+106
-87
lines changed

4 files changed

+106
-87
lines changed

Package.swift

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
// swift-tools-version: 6.0
1+
// swift-tools-version: 6.1
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

6-
#if os(macOS)
7-
let platforms: [PackageDescription.SupportedPlatform]? = [.macOS(.v15), .iOS(.v13)]
8-
#else
9-
let platforms: [PackageDescription.SupportedPlatform]? = nil
10-
#endif
11-
126
let package = Package(
137
name: "BreezeLambdaWebHook",
14-
platforms: platforms,
8+
platforms: [
9+
.macOS(.v15)
10+
],
1511
products: [
1612
.library(
1713
name: "BreezeLambdaWebHook",
@@ -23,10 +19,9 @@ let package = Package(
2319
)
2420
],
2521
dependencies: [
26-
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"),
27-
// .package(path: "../swift-aws-lambda-runtime"),
22+
.package(url: "https://github.com/andrea-scuderi/swift-aws-lambda-runtime.git", branch: "main"),
2823
.package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "0.5.0"),
29-
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.11.2"),
24+
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.22.0"),
3025
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3")
3126
],
3227
targets: [

Sources/BreezeLambdaWebHook/BreezeLambdaWebHook.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public struct BreezeLambdaWebHook<LambdaHandler: BreezeLambdaWebHookHandler>: Se
3535
)
3636
let serviceGroup = ServiceGroup(
3737
services: [lambdaService],
38-
gracefulShutdownSignals: [.sigterm],
38+
gracefulShutdownSignals: [.sigterm, .sigint],
3939
logger: config.logger
4040
)
4141
config.logger.error("Starting \(name) ...")

Sources/BreezeLambdaWebHook/BreezeLambdaWebHookService.swift

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,33 @@ public actor BreezeLambdaWebHookService<Handler: BreezeLambdaWebHookHandler>: Se
3131

3232
let config: BreezeHTTPClientConfig
3333
var handlerContext: HandlerContext?
34+
let httpClient: HTTPClient
35+
private var isStarted = false
3436

3537
public init(config: BreezeHTTPClientConfig) {
3638
self.config = config
37-
}
38-
39-
public func run() async throws {
4039
let timeout = HTTPClient.Configuration.Timeout(
4140
connect: config.timeout,
4241
read: config.timeout
4342
)
4443
let configuration = HTTPClient.Configuration(timeout: timeout)
45-
let httpClient = HTTPClient(
44+
httpClient = HTTPClient(
4645
eventLoopGroupProvider: .singleton,
4746
configuration: configuration
4847
)
48+
}
49+
50+
public func run() async throws {
51+
isStarted = true
4952
let handlerContext = HandlerContext(httpClient: httpClient)
5053
self.handlerContext = handlerContext
5154
let runtime = LambdaRuntime(body: handler)
52-
try await withGracefulShutdownHandler {
55+
try await runTaskWithCancellationOnGracefulShutdown {
5356
try await runtime.run()
5457
} onGracefulShutdown: {
55-
do {
56-
self.config.logger.info("Shutting down HTTP client...")
57-
try httpClient.syncShutdown()
58-
self.config.logger.info("HTTP client has been shut down.")
59-
} catch {
60-
self.config.logger.error("Error shutting down HTTP client: \(error)")
61-
}
58+
self.config.logger.info("Shutting down HTTP client...")
59+
_ = self.httpClient.shutdown()
60+
self.config.logger.info("HTTP client has been shut down.")
6261
}
6362
}
6463

@@ -68,5 +67,31 @@ public actor BreezeLambdaWebHookService<Handler: BreezeLambdaWebHookHandler>: Se
6867
}
6968
return try await Handler(handlerContext: handlerContext).handle(event, context: context)
7069
}
70+
71+
/// Runs a task with cancellation on graceful shutdown.
72+
///
73+
/// - Note: It's required to allow a full process shutdown without leaving tasks hanging.
74+
private func runTaskWithCancellationOnGracefulShutdown(
75+
operation: @escaping @Sendable () async throws -> Void,
76+
onGracefulShutdown: () async throws -> Void
77+
) async throws {
78+
let (cancelOrGracefulShutdown, cancelOrGracefulShutdownContinuation) = AsyncStream<Void>.makeStream()
79+
let task = Task {
80+
try await withTaskCancellationOrGracefulShutdownHandler {
81+
try await operation()
82+
} onCancelOrGracefulShutdown: {
83+
cancelOrGracefulShutdownContinuation.yield()
84+
cancelOrGracefulShutdownContinuation.finish()
85+
}
86+
}
87+
for await _ in cancelOrGracefulShutdown {
88+
try await onGracefulShutdown()
89+
task.cancel()
90+
}
91+
}
92+
93+
deinit {
94+
_ = httpClient.shutdown()
95+
}
7196
}
7297

Tests/BreezeLambdaWebHookTests/BreezeLambdaWebHookService.swift

Lines changed: 62 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -37,32 +37,32 @@ struct BreezeLambdaWebHookServiceTests {
3737
#expect(context.httpClient === httpClient)
3838
}
3939

40-
// @Test("Service creates HTTP client with correct timeout configuration")
41-
// func serviceCreatesHTTPClientWithCorrectConfig() async throws {
42-
// try await testGracefulShutdown { gracefulShutdownTestTrigger in
43-
// try await withThrowingTaskGroup(of: Void.self) { group in
44-
// let logger = Logger(label: "test")
45-
// let config = BreezeHTTPClientConfig(timeout: .seconds(30), logger: logger)
46-
// let sut = BreezeLambdaWebHookService<MockHandler>(config: config)
47-
//
48-
// await withTaskCancellationOrGracefulShutdownHandler {
49-
// group.addTask {
50-
// try await sut.run()
51-
// }
52-
// } onCancelOrGracefulShutdown: {
53-
// gracefulShutdownTestTrigger.triggerGracefulShutdown()
54-
// logger.info("On Graceful Shutdown")
55-
// }
56-
//
57-
// try await Task.sleep(nanoseconds: 1_000_000_000)
58-
// group.cancelAll()
59-
//
60-
// let handlerContext = try #require(await sut.handlerContext)
61-
// #expect(handlerContext.httpClient.configuration.timeout.read == .seconds(30))
62-
// #expect(handlerContext.httpClient.configuration.timeout.connect == .seconds(30))
63-
// }
64-
// }
65-
// }
40+
@Test("Service creates HTTP client with correct timeout configuration")
41+
func serviceCreatesHTTPClientWithCorrectConfig() async throws {
42+
try await testGracefulShutdown { gracefulShutdownTestTrigger in
43+
try await withThrowingTaskGroup(of: Void.self) { group in
44+
let logger = Logger(label: "test")
45+
let config = BreezeHTTPClientConfig(timeout: .seconds(30), logger: logger)
46+
let sut = BreezeLambdaWebHookService<MockHandler>(config: config)
47+
48+
await withTaskCancellationOrGracefulShutdownHandler {
49+
group.addTask {
50+
try await sut.run()
51+
}
52+
} onCancelOrGracefulShutdown: {
53+
gracefulShutdownTestTrigger.triggerGracefulShutdown()
54+
logger.info("On Graceful Shutdown")
55+
}
56+
57+
try await Task.sleep(nanoseconds: 1_000_000_000)
58+
group.cancelAll()
59+
60+
let handlerContext = try #require(await sut.handlerContext)
61+
#expect(handlerContext.httpClient.configuration.timeout.read == .seconds(30))
62+
#expect(handlerContext.httpClient.configuration.timeout.connect == .seconds(30))
63+
}
64+
}
65+
}
6666

6767
@Test("Handler throws when handlerContext is nil")
6868
func handlerThrowsWhenContextIsNil() async throws {
@@ -79,43 +79,42 @@ struct BreezeLambdaWebHookServiceTests {
7979
}
8080
}
8181

82-
// @Test("Handler delegates to specific handler implementation")
83-
// func handlerDelegatesToImplementation() async throws {
84-
// try await testGracefulShutdown { gracefulShutdownTestTrigger in
85-
// try await withThrowingTaskGroup(of: Void.self) { group in
86-
//
87-
// let logger = Logger(label: "test")
88-
// let config = BreezeHTTPClientConfig(timeout: .seconds(30), logger: logger)
89-
// let sut = BreezeLambdaWebHookService<MockHandler>(config: config)
90-
//
91-
// group.addTask {
92-
// try await withGracefulShutdownHandler {
93-
// try await sut.run()
94-
// } onGracefulShutdown: {
95-
// logger.info("On Graceful Shutdown")
96-
// }
97-
// }
98-
// group.addTask {
99-
// try await Task.sleep(nanoseconds: 1_000_000_000)
100-
// gracefulShutdownTestTrigger.triggerGracefulShutdown()
101-
// }
102-
//
103-
// let createRequest = try Fixtures.fixture(name: Fixtures.getWebHook, type: "json")
104-
// let event = try decoder.decode(APIGatewayV2Request.self, from: createRequest)
105-
// let context = LambdaContext(requestID: "req1", traceID: "trace1", invokedFunctionARN: "", deadline: .now(), logger: logger)
106-
//
107-
// let response = try await sut.handler(event: event, context: context)
108-
// let handlerContext = try #require(await sut.handlerContext)
109-
// #expect(response.statusCode == 200)
110-
// #expect(response.body == "Mock response")
111-
// #expect(handlerContext.httpClient.configuration.timeout.read == .seconds(30))
112-
// #expect(handlerContext.httpClient.configuration.timeout.connect == .seconds(30))
113-
//
114-
//
115-
// group.cancelAll()
116-
// }
117-
// }
118-
// }
82+
@Test("Handler delegates to specific handler implementation")
83+
func handlerDelegatesToImplementation() async throws {
84+
try await testGracefulShutdown { gracefulShutdownTestTrigger in
85+
try await withThrowingTaskGroup(of: Void.self) { group in
86+
87+
let logger = Logger(label: "test")
88+
let config = BreezeHTTPClientConfig(timeout: .seconds(30), logger: logger)
89+
let sut = BreezeLambdaWebHookService<MockHandler>(config: config)
90+
91+
group.addTask {
92+
try await withGracefulShutdownHandler {
93+
try await sut.run()
94+
} onGracefulShutdown: {
95+
logger.info("On Graceful Shutdown")
96+
}
97+
}
98+
group.addTask {
99+
try await Task.sleep(nanoseconds: 1_000_000_000)
100+
gracefulShutdownTestTrigger.triggerGracefulShutdown()
101+
}
102+
103+
let createRequest = try Fixtures.fixture(name: Fixtures.getWebHook, type: "json")
104+
let event = try decoder.decode(APIGatewayV2Request.self, from: createRequest)
105+
let context = LambdaContext(requestID: "req1", traceID: "trace1", invokedFunctionARN: "", deadline: .now(), logger: logger)
106+
107+
let response = try await sut.handler(event: event, context: context)
108+
let handlerContext = try #require(await sut.handlerContext)
109+
#expect(response.statusCode == 200)
110+
#expect(response.body == "Mock response")
111+
#expect(handlerContext.httpClient.configuration.timeout.read == .seconds(30))
112+
#expect(handlerContext.httpClient.configuration.timeout.connect == .seconds(30))
113+
114+
group.cancelAll()
115+
}
116+
}
117+
}
119118
}
120119

121120
struct MockHandler: BreezeLambdaWebHookHandler {

0 commit comments

Comments
 (0)