diff --git a/Sources/GRPCHealthService/Health.swift b/Sources/GRPCHealthService/Health.swift deleted file mode 100644 index c2cfbc5..0000000 --- a/Sources/GRPCHealthService/Health.swift +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2024, 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. - */ - -public import GRPCCore - -/// ``Health`` is gRPC’s mechanism for checking whether a server is able to handle RPCs. Its semantics are documented in -/// https://github.com/grpc/grpc/blob/5011420f160b91129a7baebe21df9444a07896a6/doc/health-checking.md. -/// -/// `Health` initializes a new ``Health/Service-swift.struct`` and ``Health/Provider-swift.struct``. -/// - `Health.Service` implements the Health service from the `grpc.health.v1` package and can be registered with a server -/// like any other service. -/// - `Health.Provider` provides status updates to `Health.Service`. `Health.Service` doesn't know about the other -/// services running on a server so it must be provided with status updates via `Health.Provider`. To make specifying the service -/// being updated easier, the generated code for services includes an extension to `ServiceDescriptor`. -/// -/// The following shows an example of initializing a Health service and updating the status of the `Foo` service in the `bar` package. -/// -/// ```swift -/// let health = Health() -/// let server = GRPCServer( -/// transport: transport, -/// services: [health.service, FooService()] -/// ) -/// -/// health.provider.updateStatus( -/// .serving, -/// forService: .bar_Foo -/// ) -/// ``` -public struct Health: Sendable { - /// An implementation of the `grpc.health.v1.Health` service. - public let service: Health.Service - - /// Provides status updates to the Health service. - public let provider: Health.Provider - - /// Constructs a new ``Health``, initializing a ``Health/Service-swift.struct`` and a - /// ``Health/Provider-swift.struct``. - public init() { - let healthService = HealthService() - - self.service = Health.Service(healthService: healthService) - self.provider = Health.Provider(healthService: healthService) - } -} - -extension Health { - /// An implementation of the `grpc.health.v1.Health` service. - public struct Service: RegistrableRPCService, Sendable { - private let healthService: HealthService - - public func registerMethods(with router: inout RPCRouter) { - self.healthService.registerMethods(with: &router) - } - - fileprivate init(healthService: HealthService) { - self.healthService = healthService - } - } - - /// Provides status updates to ``Health/Service-swift.struct``. - public struct Provider: Sendable { - private let healthService: HealthService - - /// Updates the status of a service. - /// - /// - Parameters: - /// - status: The status of the service. - /// - service: The description of the service. - public func updateStatus( - _ status: ServingStatus, - forService service: ServiceDescriptor - ) { - self.healthService.updateStatus( - Grpc_Health_V1_HealthCheckResponse.ServingStatus(status), - forService: service.fullyQualifiedService - ) - } - - /// Updates the status of a service. - /// - /// - Parameters: - /// - status: The status of the service. - /// - service: The fully qualified service name in the format: - /// - "package.service": if the service is part of a package. For example, "helloworld.Greeter". - /// - "service": if the service is not part of a package. For example, "Greeter". - public func updateStatus( - _ status: ServingStatus, - forService service: String - ) { - self.healthService.updateStatus( - Grpc_Health_V1_HealthCheckResponse.ServingStatus(status), - forService: service - ) - } - - fileprivate init(healthService: HealthService) { - self.healthService = healthService - } - } -} - -extension Grpc_Health_V1_HealthCheckResponse.ServingStatus { - package init(_ status: ServingStatus) { - switch status.value { - case .serving: - self = .serving - case .notServing: - self = .notServing - } - } -} diff --git a/Sources/GRPCHealthService/HealthService+Service.swift b/Sources/GRPCHealthService/HealthService+Service.swift new file mode 100644 index 0000000..165d1c8 --- /dev/null +++ b/Sources/GRPCHealthService/HealthService+Service.swift @@ -0,0 +1,135 @@ +/* + * Copyright 2024, 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. + */ + +internal import GRPCCore +private import Synchronization + +extension HealthService { + internal struct Service: Grpc_Health_V1_Health.ServiceProtocol { + private let state = Self.State() + } +} + +extension HealthService.Service { + func check( + request: ServerRequest, + context: ServerContext + ) async throws -> ServerResponse { + let service = request.message.service + + guard let status = self.state.currentStatus(ofService: service) else { + throw RPCError(code: .notFound, message: "Requested service unknown.") + } + + var response = Grpc_Health_V1_HealthCheckResponse() + response.status = status + + return ServerResponse(message: response) + } + + func watch( + request: ServerRequest, + context: ServerContext + ) async -> StreamingServerResponse { + let service = request.message.service + let statuses = AsyncStream.makeStream(of: Grpc_Health_V1_HealthCheckResponse.ServingStatus.self) + + self.state.addContinuation(statuses.continuation, forService: service) + + return StreamingServerResponse(of: Grpc_Health_V1_HealthCheckResponse.self) { writer in + var response = Grpc_Health_V1_HealthCheckResponse() + + for await status in statuses.stream { + response.status = status + try await writer.write(response) + } + + return [:] + } + } + + func updateStatus( + _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus, + forService service: String + ) { + self.state.updateStatus(status, forService: service) + } +} + +extension HealthService.Service { + private final class State: Sendable { + // The state of each service keyed by the fully qualified service name. + private let lockedStorage = Mutex([String: ServiceState]()) + + fileprivate func currentStatus( + ofService service: String + ) -> Grpc_Health_V1_HealthCheckResponse.ServingStatus? { + return self.lockedStorage.withLock { $0[service]?.currentStatus } + } + + fileprivate func updateStatus( + _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus, + forService service: String + ) { + self.lockedStorage.withLock { storage in + storage[service, default: ServiceState(status: status)].updateStatus(status) + } + } + + fileprivate func addContinuation( + _ continuation: AsyncStream.Continuation, + forService service: String + ) { + self.lockedStorage.withLock { storage in + storage[service, default: ServiceState(status: .serviceUnknown)] + .addContinuation(continuation) + } + } + } + + // Encapsulates the current status of a service and the continuations of its watch streams. + private struct ServiceState: Sendable { + private(set) var currentStatus: Grpc_Health_V1_HealthCheckResponse.ServingStatus + private var continuations: + [AsyncStream.Continuation] + + fileprivate mutating func updateStatus( + _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus + ) { + guard status != self.currentStatus else { + return + } + + self.currentStatus = status + + for continuation in self.continuations { + continuation.yield(status) + } + } + + fileprivate mutating func addContinuation( + _ continuation: AsyncStream.Continuation + ) { + self.continuations.append(continuation) + continuation.yield(self.currentStatus) + } + + fileprivate init(status: Grpc_Health_V1_HealthCheckResponse.ServingStatus = .unknown) { + self.currentStatus = status + self.continuations = [] + } + } +} diff --git a/Sources/GRPCHealthService/HealthService.swift b/Sources/GRPCHealthService/HealthService.swift index f548840..1cf1468 100644 --- a/Sources/GRPCHealthService/HealthService.swift +++ b/Sources/GRPCHealthService/HealthService.swift @@ -14,118 +14,102 @@ * limitations under the License. */ -internal import GRPCCore -private import Synchronization - -internal struct HealthService: Grpc_Health_V1_Health.ServiceProtocol { - private let state = HealthService.State() - - func check( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - let service = request.message.service - - guard let status = self.state.currentStatus(ofService: service) else { - throw RPCError(code: .notFound, message: "Requested service unknown.") - } - - var response = Grpc_Health_V1_HealthCheckResponse() - response.status = status - - return ServerResponse(message: response) - } - - func watch( - request: ServerRequest, - context: ServerContext - ) async -> StreamingServerResponse { - let service = request.message.service - let statuses = AsyncStream.makeStream(of: Grpc_Health_V1_HealthCheckResponse.ServingStatus.self) - - self.state.addContinuation(statuses.continuation, forService: service) - - return StreamingServerResponse(of: Grpc_Health_V1_HealthCheckResponse.self) { writer in - var response = Grpc_Health_V1_HealthCheckResponse() - - for await status in statuses.stream { - response.status = status - try await writer.write(response) - } - - return [:] - } +public import GRPCCore + +/// ``HealthService`` is gRPC's mechanism for checking whether a server is able to handle RPCs. +/// Its semantics are documented in the [gRPC repository]( https://github.com/grpc/grpc/blob/5011420f160b91129a7baebe21df9444a07896a6/doc/health-checking.md). +/// +/// `HealthService` implements the `grpc.health.v1` service and can be registered with a server +/// like any other service. It holds a ``HealthService/Provider-swift.struct`` which provides +/// status updates to the service. The service doesn't know about the other services running on the +/// server so it must be provided with status updates via the ``Provider-swift.struct``. To make +/// specifying the service being updated easier, the generated code for services includes an +/// extension to `ServiceDescriptor`. +/// +/// The following shows an example of initializing a Health service and updating the status of +/// the `Foo` service in the `bar` package. +/// +/// ```swift +/// let health = HealthService() +/// try await withGRPCServer( +/// transport: transport, +/// services: [health, FooService()] +/// ) { server in +/// // Update the status of the 'bar.Foo' service. +/// health.provider.updateStatus(.serving, forService: .bar_Foo) +/// +/// // ... +/// } +/// ``` +public struct HealthService: Sendable, RegistrableRPCService { + /// An implementation of the `grpc.health.v1.Health` service. + private let service: Service + + /// Provides status updates to the Health service. + public let provider: HealthService.Provider + + /// Constructs a new ``HealthService``. + public init() { + let healthService = Service() + self.service = healthService + self.provider = HealthService.Provider(healthService: healthService) } - func updateStatus( - _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus, - forService service: String - ) { - self.state.updateStatus(status, forService: service) + public func registerMethods(with router: inout RPCRouter) { + self.service.registerMethods(with: &router) } } extension HealthService { - private final class State: Sendable { - // The state of each service keyed by the fully qualified service name. - private let lockedStorage = Mutex([String: ServiceState]()) - - fileprivate func currentStatus( - ofService service: String - ) -> Grpc_Health_V1_HealthCheckResponse.ServingStatus? { - return self.lockedStorage.withLock { $0[service]?.currentStatus } - } - - fileprivate func updateStatus( - _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus, - forService service: String + /// Provides status updates to ``HealthService``. + public struct Provider: Sendable { + private let healthService: Service + + /// Updates the status of a service. + /// + /// - Parameters: + /// - status: The status of the service. + /// - service: The description of the service. + public func updateStatus( + _ status: ServingStatus, + forService service: ServiceDescriptor ) { - self.lockedStorage.withLock { storage in - storage[service, default: ServiceState(status: status)].updateStatus(status) - } + self.healthService.updateStatus( + Grpc_Health_V1_HealthCheckResponse.ServingStatus(status), + forService: service.fullyQualifiedService + ) } - fileprivate func addContinuation( - _ continuation: AsyncStream.Continuation, + /// Updates the status of a service. + /// + /// - Parameters: + /// - status: The status of the service. + /// - service: The fully qualified service name in the format: + /// - "package.service": if the service is part of a package. For example, "helloworld.Greeter". + /// - "service": if the service is not part of a package. For example, "Greeter". + public func updateStatus( + _ status: ServingStatus, forService service: String ) { - self.lockedStorage.withLock { storage in - storage[service, default: ServiceState(status: .serviceUnknown)] - .addContinuation(continuation) - } + self.healthService.updateStatus( + Grpc_Health_V1_HealthCheckResponse.ServingStatus(status), + forService: service + ) } - } - - // Encapsulates the current status of a service and the continuations of its watch streams. - private struct ServiceState: Sendable { - private(set) var currentStatus: Grpc_Health_V1_HealthCheckResponse.ServingStatus - private var continuations: - [AsyncStream.Continuation] - - fileprivate mutating func updateStatus( - _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus - ) { - guard status != self.currentStatus else { - return - } - self.currentStatus = status - - for continuation in self.continuations { - continuation.yield(status) - } - } - - fileprivate mutating func addContinuation( - _ continuation: AsyncStream.Continuation - ) { - self.continuations.append(continuation) - continuation.yield(self.currentStatus) + fileprivate init(healthService: Service) { + self.healthService = healthService } + } +} - fileprivate init(status: Grpc_Health_V1_HealthCheckResponse.ServingStatus = .unknown) { - self.currentStatus = status - self.continuations = [] +extension Grpc_Health_V1_HealthCheckResponse.ServingStatus { + package init(_ status: ServingStatus) { + switch status.value { + case .serving: + self = .serving + case .notServing: + self = .notServing } } } diff --git a/Sources/GRPCReflectionService/Service/ReflectionService.swift b/Sources/GRPCReflectionService/Service/ReflectionService.swift index fc0eebe..cb77a0b 100644 --- a/Sources/GRPCReflectionService/Service/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Service/ReflectionService.swift @@ -33,13 +33,13 @@ public import struct Foundation.Data /// /// The service will offer information to clients about any registered services. You can register /// a service by providing its descriptor set to the service. -public final class ReflectionService: Sendable { +public struct ReflectionService: Sendable { private let service: ReflectionService.V1 /// Create a new instance of the reflection service from a list of descriptor set file URLs. /// /// - Parameter fileURLs: A list of file URLs containing serialized descriptor sets. - public convenience init( + public init( descriptorSetFileURLs fileURLs: [URL] ) throws { let fileDescriptorProtos = try Self.readDescriptorSets(atURLs: fileURLs) @@ -49,7 +49,7 @@ public final class ReflectionService: Sendable { /// Create a new instance of the reflection service from a list of descriptor set file paths. /// /// - Parameter filePaths: A list of file paths containing serialized descriptor sets. - public convenience init( + public init( descriptorSetFilePaths filePaths: [String] ) throws { let fileDescriptorProtos = try Self.readDescriptorSets(atPaths: filePaths) diff --git a/Tests/GRPCHealthServiceTests/HealthTests.swift b/Tests/GRPCHealthServiceTests/HealthTests.swift index e628031..8571557 100644 --- a/Tests/GRPCHealthServiceTests/HealthTests.swift +++ b/Tests/GRPCHealthServiceTests/HealthTests.swift @@ -22,11 +22,11 @@ import XCTest final class HealthTests: XCTestCase { private func withHealthClient( - _ body: @Sendable (Grpc_Health_V1_Health.Client, Health.Provider) async throws -> Void + _ body: @Sendable (Grpc_Health_V1_Health.Client, HealthService.Provider) async throws -> Void ) async throws { - let health = Health() + let health = HealthService() let inProcess = InProcessTransport() - let server = GRPCServer(transport: inProcess.server, services: [health.service]) + let server = GRPCServer(transport: inProcess.server, services: [health]) let client = GRPCClient(transport: inProcess.client) let healthClient = Grpc_Health_V1_Health.Client(wrapping: client)