Skip to content

Commit 0eb173d

Browse files
authored
Add GRPCServiceLifecycle module (#29)
This new module adds conformances to the `Service` protocol from `grpc-swift-service-lifecycle` to `GRPCClient` and `GRPCServer` so that they compose nicely with `ServiceLifecycle`. *Note: depends on grpc/grpc-swift#2166
1 parent 0cc94cc commit 0eb173d

File tree

5 files changed

+150
-2
lines changed

5 files changed

+150
-2
lines changed

Package.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ let products: [Product] = [
3030
name: "GRPCInterceptors",
3131
targets: ["GRPCInterceptors"]
3232
),
33+
.library(
34+
name: "GRPCServiceLifecycle",
35+
targets: ["GRPCServiceLifecycle"]
36+
),
3337
.library(
3438
name: "GRPCInteropTests",
3539
targets: ["GRPCInteropTests"]
@@ -39,7 +43,7 @@ let products: [Product] = [
3943
let dependencies: [Package.Dependency] = [
4044
.package(
4145
url: "https://github.com/grpc/grpc-swift.git",
42-
exact: "2.0.0-beta.3"
46+
branch: "main"
4347
),
4448
.package(
4549
url: "https://github.com/grpc/grpc-swift-protobuf.git",
@@ -53,6 +57,10 @@ let dependencies: [Package.Dependency] = [
5357
url: "https://github.com/apple/swift-distributed-tracing.git",
5458
from: "1.1.2"
5559
),
60+
.package(
61+
url: "https://github.com/swift-server/swift-service-lifecycle.git",
62+
from: "2.6.3"
63+
),
5664
]
5765

5866
let defaultSwiftSettings: [SwiftSetting] = [
@@ -126,6 +134,26 @@ let targets: [Target] = [
126134
swiftSettings: defaultSwiftSettings
127135
),
128136

137+
// Retroactive conformances of gRPC client and server to swift-server-lifecycle's Service.
138+
.target(
139+
name: "GRPCServiceLifecycle",
140+
dependencies: [
141+
.product(name: "GRPCCore", package: "grpc-swift"),
142+
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
143+
],
144+
swiftSettings: defaultSwiftSettings
145+
),
146+
.testTarget(
147+
name: "GRPCServiceLifecycleTests",
148+
dependencies: [
149+
.target(name: "GRPCServiceLifecycle"),
150+
.product(name: "GRPCCore", package: "grpc-swift"),
151+
.product(name: "ServiceLifecycleTestKit", package: "swift-service-lifecycle"),
152+
.product(name: "GRPCInProcessTransport", package: "grpc-swift"),
153+
],
154+
swiftSettings: defaultSwiftSettings
155+
),
156+
129157
// gRPC interop test implementation.
130158
.target(
131159
name: "GRPCInteropTests",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2025, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
public import GRPCCore
18+
public import ServiceLifecycle
19+
20+
// A `@retroactive` conformance here is okay because this project is also owned by the owners of
21+
// `GRPCCore`, and thus, the owners of `GRPCClient`. A conflicting conformance won't be added.
22+
extension GRPCClient: @retroactive Service {
23+
public func run() async throws {
24+
try await withGracefulShutdownHandler {
25+
try await self.runConnections()
26+
} onGracefulShutdown: {
27+
self.beginGracefulShutdown()
28+
}
29+
}
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2025, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
public import GRPCCore
18+
public import ServiceLifecycle
19+
20+
// A `@retroactive` conformance here is okay because this project is also owned by the owners of
21+
// `GRPCCore`, and thus, the owners of `GRPCServer`. A conflicting conformance won't be added.
22+
extension GRPCServer: @retroactive Service {
23+
public func run() async throws {
24+
try await withGracefulShutdownHandler {
25+
try await self.serve()
26+
} onGracefulShutdown: {
27+
self.beginGracefulShutdown()
28+
}
29+
}
30+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2025, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import GRPCCore
18+
import GRPCInProcessTransport
19+
import GRPCServiceLifecycle
20+
import ServiceLifecycleTestKit
21+
import Testing
22+
23+
@Suite("gRPC ServiceLifecycle/Service conformance tests")
24+
struct ServiceLifecycleConformanceTests {
25+
@Test("Client respects graceful shutdown")
26+
func clientGracefulShutdown() async throws {
27+
let inProcess = InProcessTransport()
28+
try await testGracefulShutdown { trigger in
29+
try await withThrowingDiscardingTaskGroup { group in
30+
group.addTask {
31+
let client = GRPCClient(transport: inProcess.client)
32+
try await client.run()
33+
}
34+
35+
group.addTask {
36+
try await Task.sleep(for: .milliseconds(10))
37+
trigger.triggerGracefulShutdown()
38+
}
39+
}
40+
}
41+
}
42+
43+
@Test("Server respects graceful shutdown")
44+
func serverGracefulShutdown() async throws {
45+
let inProcess = InProcessTransport()
46+
try await testGracefulShutdown { trigger in
47+
try await withThrowingDiscardingTaskGroup { group in
48+
group.addTask {
49+
let server = GRPCServer(transport: inProcess.server, services: [])
50+
try await server.run()
51+
}
52+
53+
group.addTask {
54+
try await Task.sleep(for: .milliseconds(10))
55+
trigger.triggerGracefulShutdown()
56+
}
57+
}
58+
}
59+
}
60+
}

Tests/InProcessInteropTests/InProcessInteroperabilityTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ final class InProcessInteroperabilityTests: XCTestCase {
3535
try await withThrowingTaskGroup(of: Void.self) { clientGroup in
3636
let client = GRPCClient(transport: inProcess.client)
3737
clientGroup.addTask {
38-
try await client.run()
38+
try await client.runConnections()
3939
}
4040
try await interopTestCase.makeTest().run(client: client)
4141

0 commit comments

Comments
 (0)