diff --git a/Examples/service-lifecycle/Package.swift b/Examples/service-lifecycle/Package.swift new file mode 100644 index 000000000..c2d239c21 --- /dev/null +++ b/Examples/service-lifecycle/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version:6.0 +/* + * Copyright 2025, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import PackageDescription + +let package = Package( + name: "service-lifecycle", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-extras", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "service-lifecycle", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCInProcessTransport", package: "grpc-swift"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + .product(name: "GRPCServiceLifecycle", package: "grpc-swift-extras"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/Examples/service-lifecycle/README.md b/Examples/service-lifecycle/README.md new file mode 100644 index 000000000..db1b12b27 --- /dev/null +++ b/Examples/service-lifecycle/README.md @@ -0,0 +1,41 @@ +# Service Lifecycle + +This example demonstrates gRPC Swift's integration with Swift Service Lifecycle +which is provided by the gRPC Swift Extras package. + +## Overview + +A "service-lifecycle" command line tool that uses generated stubs for a +'greeter' service starts an in-process client and server orchestrated using +Swift Service Lifecycle. The client makes requests against the server which +periodically changes its greeting. + +## Prerequisites + +You must have the Protocol Buffers compiler (`protoc`) installed. You can find +the instructions for doing this in the [gRPC Swift Protobuf documentation][0]. +The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`, +this is to let the build system know where `protoc` is located so that it can +generate stubs for you. You can read more about it in the [gRPC Swift Protobuf +documentation][1]. + +## Usage + +Build and run the server using the CLI: + +```console +$ PROTOC_PATH=$(which protoc) swift run service-lifecycle +Здравствуйте, request-1! +नमस्ते, request-2! +你好, request-3! +French, request-4! +Olá, request-5! +Hola, request-6! +Hello, request-7! +Hello, request-8! +नमस्ते, request-9! +Hello, request-10! +``` + +[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc +[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs diff --git a/Examples/service-lifecycle/Sources/GreetingService.swift b/Examples/service-lifecycle/Sources/GreetingService.swift new file mode 100644 index 000000000..8ca4332fb --- /dev/null +++ b/Examples/service-lifecycle/Sources/GreetingService.swift @@ -0,0 +1,80 @@ +/* + * Copyright 2025, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GRPCCore +import ServiceLifecycle +import Synchronization + +/// Implements the "Hello World" gRPC service but modifies the greeting on a timer. +/// +/// The service conforms to the 'ServiceLifecycle.Service' and uses its 'run()' method +/// to execute the run loop which updates the greeting. +final class GreetingService { + private let updateInterval: Duration + private let currentGreetingIndex: Mutex + private let greetings: [String] = [ + "Hello", + "你好", + "नमस्ते", + "Hola", + "French", + "Olá", + "Здравствуйте", + "こんにちは", + "Ciao", + ] + + private func personalizedGreeting(forName name: String) -> String { + let index = self.currentGreetingIndex.withLock { $0 } + return "\(self.greetings[index]), \(name)!" + } + + private func periodicallyUpdateGreeting() async throws { + while !Task.isShuttingDownGracefully { + try await Task.sleep(for: self.updateInterval) + + // Increment the greeting index. + self.currentGreetingIndex.withLock { index in + // '!' is fine; greetings is non-empty. + index = self.greetings.indices.randomElement()! + } + } + } + + init(updateInterval: Duration) { + // '!' is fine; greetings is non-empty. + let index = self.greetings.indices.randomElement()! + self.currentGreetingIndex = Mutex(index) + self.updateInterval = updateInterval + } +} + +extension GreetingService: Helloworld_Greeter.SimpleServiceProtocol { + func sayHello( + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { + return .with { + $0.message = self.personalizedGreeting(forName: request.name) + } + } +} + +extension GreetingService: Service { + func run() async throws { + try await self.periodicallyUpdateGreeting() + } +} diff --git a/Examples/service-lifecycle/Sources/LifecycleExample.swift b/Examples/service-lifecycle/Sources/LifecycleExample.swift new file mode 100644 index 000000000..75a8573a0 --- /dev/null +++ b/Examples/service-lifecycle/Sources/LifecycleExample.swift @@ -0,0 +1,74 @@ +/* + * Copyright 2025, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GRPCCore +import GRPCInProcessTransport +import GRPCServiceLifecycle +import Logging +import ServiceLifecycle + +@main +struct LifecycleExample { + static func main() async throws { + // Create the gRPC service. It periodically changes the greeting returned to the client. + // It also conforms to 'ServiceLifecycle.Service' and uses the 'run()' method to perform + // the updates. + // + // A more realistic service may use the run method to maintain a connection to an upstream + // service or database. + let greetingService = GreetingService(updateInterval: .microseconds(250)) + + // Create the client and server using the in-process transport (which is used here for + // simplicity.) + let inProcess = InProcessTransport() + let server = GRPCServer(transport: inProcess.server, services: [greetingService]) + let client = GRPCClient(transport: inProcess.client) + + // Configure the service group with the services. They're started in the order they're listed + // and shutdown in reverse order. + let serviceGroup = ServiceGroup( + services: [ + greetingService, + server, + client, + ], + logger: Logger(label: "io.grpc.examples.service-lifecycle") + ) + + try await withThrowingDiscardingTaskGroup { group in + // Run the service group in a task group. This isn't typically required but is here in + // order to make requests using the client while the service group is running. + group.addTask { + try await serviceGroup.run() + } + + // Make some requests, pausing between each to give the server a chance to update + // the greeting. + let greeter = Helloworld_Greeter.Client(wrapping: client) + for request in 1 ... 10 { + let reply = try await greeter.sayHello(.with { $0.name = "request-\(request)" }) + print(reply.message) + + // Sleep for a moment. + let waitTime = Duration.milliseconds((50 ... 400).randomElement()!) + try await Task.sleep(for: waitTime) + } + + // Finally, shutdown the service group gracefully. + await serviceGroup.triggerGracefulShutdown() + } + } +} diff --git a/Examples/service-lifecycle/Sources/Protos/grpc-swift-proto-generator-config.json b/Examples/service-lifecycle/Sources/Protos/grpc-swift-proto-generator-config.json new file mode 100644 index 000000000..e6dda31fb --- /dev/null +++ b/Examples/service-lifecycle/Sources/Protos/grpc-swift-proto-generator-config.json @@ -0,0 +1,7 @@ +{ + "generate": { + "clients": true, + "servers": true, + "messages": true + } +} diff --git a/Examples/service-lifecycle/Sources/Protos/helloworld.proto b/Examples/service-lifecycle/Sources/Protos/helloworld.proto new file mode 120000 index 000000000..f4684af4f --- /dev/null +++ b/Examples/service-lifecycle/Sources/Protos/helloworld.proto @@ -0,0 +1 @@ +../../../../dev/protos/upstream/grpc/examples/helloworld.proto \ No newline at end of file