|
|
@@ -34,18 +34,22 @@ import Atomics
|
|
|
/// The following example demonstrates how to create and configure a server.
|
|
|
///
|
|
|
/// ```swift
|
|
|
-/// let server = GRPCServer()
|
|
|
-///
|
|
|
-/// // Create and add an in-process transport.
|
|
|
+/// // Create and an in-process transport.
|
|
|
/// let inProcessTransport = InProcessServerTransport()
|
|
|
-/// server.transports.add(inProcessTransport)
|
|
|
///
|
|
|
-/// // Create and register the 'Greeter' and 'Echo' services.
|
|
|
-/// server.services.register(GreeterService())
|
|
|
-/// server.services.register(EchoService())
|
|
|
+/// // Create the 'Greeter' and 'Echo' services.
|
|
|
+/// let greeter = GreeterService()
|
|
|
+/// let echo = EchoService()
|
|
|
+///
|
|
|
+/// // Create an interceptor.
|
|
|
+/// let statsRecorder = StatsRecordingServerInterceptors()
|
|
|
///
|
|
|
-/// // Create and add some interceptors.
|
|
|
-/// server.interceptors.add(StatsRecordingServerInterceptors())
|
|
|
+/// // Finally create the server.
|
|
|
+/// let server = GRPCServer(
|
|
|
+/// transports: [inProcessTransport],
|
|
|
+/// services: [greeter, echo],
|
|
|
+/// interceptors: [statsRecorder]
|
|
|
+/// )
|
|
|
/// ```
|
|
|
///
|
|
|
/// ## Starting and stopping the server
|
|
|
@@ -66,29 +70,15 @@ import Atomics
|
|
|
/// that need their lifecycles managed you should consider using [Swift Service
|
|
|
/// Lifecycle](https://github.com/swift-server/swift-service-lifecycle).
|
|
|
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
|
|
-public final class GRPCServer: Sendable {
|
|
|
+public struct GRPCServer: Sendable {
|
|
|
typealias Stream = RPCStream<ServerTransport.Inbound, ServerTransport.Outbound>
|
|
|
|
|
|
/// A collection of ``ServerTransport`` implementations that the server uses to listen
|
|
|
/// for new requests.
|
|
|
- public var transports: Transports {
|
|
|
- get {
|
|
|
- self.storage.withLockedValue { $0.transports }
|
|
|
- }
|
|
|
- set {
|
|
|
- self.storage.withLockedValue { $0.transports = newValue }
|
|
|
- }
|
|
|
- }
|
|
|
+ private let transports: [any ServerTransport]
|
|
|
|
|
|
/// The services registered which the server is serving.
|
|
|
- public var services: Services {
|
|
|
- get {
|
|
|
- self.storage.withLockedValue { $0.services }
|
|
|
- }
|
|
|
- set {
|
|
|
- self.storage.withLockedValue { $0.services = newValue }
|
|
|
- }
|
|
|
- }
|
|
|
+ private let router: RPCRouter
|
|
|
|
|
|
/// A collection of ``ServerInterceptor`` implementations which are applied to all accepted
|
|
|
/// RPCs.
|
|
|
@@ -96,34 +86,12 @@ public final class GRPCServer: Sendable {
|
|
|
/// RPCs are intercepted in the order that interceptors are added. That is, a request received
|
|
|
/// from the client will first be intercepted by the first added interceptor followed by the
|
|
|
/// second, and so on.
|
|
|
- public var interceptors: Interceptors {
|
|
|
- get {
|
|
|
- self.storage.withLockedValue { $0.interceptors }
|
|
|
- }
|
|
|
- set {
|
|
|
- self.storage.withLockedValue { $0.interceptors = newValue }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// Underlying storage for the server.
|
|
|
- private struct Storage {
|
|
|
- var transports: Transports
|
|
|
- var services: Services
|
|
|
- var interceptors: Interceptors
|
|
|
- var state: State
|
|
|
-
|
|
|
- init() {
|
|
|
- self.transports = Transports()
|
|
|
- self.services = Services()
|
|
|
- self.interceptors = Interceptors()
|
|
|
- self.state = .notStarted
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private let storage: LockedValueBox<Storage>
|
|
|
+ private let interceptors: [any ServerInterceptor]
|
|
|
|
|
|
/// The state of the server.
|
|
|
- private enum State {
|
|
|
+ private let state: ManagedAtomic<State>
|
|
|
+
|
|
|
+ private enum State: UInt8, AtomicValue {
|
|
|
/// The server hasn't been started yet. Can transition to `starting` or `stopped`.
|
|
|
case notStarted
|
|
|
/// The server is starting but isn't accepting requests yet. Can transition to `running`
|
|
|
@@ -141,11 +109,46 @@ public final class GRPCServer: Sendable {
|
|
|
|
|
|
/// Creates a new server with no resources.
|
|
|
///
|
|
|
- /// You can add resources to the server via ``transports-swift.property``,
|
|
|
- /// ``services-swift.property``, and ``interceptors-swift.property`` and start the server by
|
|
|
- /// calling ``run()``. Any changes to resources after ``run()`` has been called will be ignored.
|
|
|
- public init() {
|
|
|
- self.storage = LockedValueBox(Storage())
|
|
|
+ /// - Parameters:
|
|
|
+ /// - transports: The transports the server should listen on.
|
|
|
+ /// - services: Services offered by the server.
|
|
|
+ /// - interceptors: A collection of interceptors providing cross-cutting functionality to each
|
|
|
+ /// accepted RPC. The order in which interceptors are added reflects the order in which they
|
|
|
+ /// are called. The first interceptor added will be the first interceptor to intercept each
|
|
|
+ /// request. The last interceptor added will be the final interceptor to intercept each
|
|
|
+ /// request before calling the appropriate handler.
|
|
|
+ public init(
|
|
|
+ transports: [any ServerTransport],
|
|
|
+ services: [any RegistrableRPCService],
|
|
|
+ interceptors: [any ServerInterceptor] = []
|
|
|
+ ) {
|
|
|
+ var router = RPCRouter()
|
|
|
+ for service in services {
|
|
|
+ service.registerMethods(with: &router)
|
|
|
+ }
|
|
|
+
|
|
|
+ self.init(transports: transports, router: router, interceptors: interceptors)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Creates a new server with no resources.
|
|
|
+ ///
|
|
|
+ /// - Parameters:
|
|
|
+ /// - transports: The transports the server should listen on.
|
|
|
+ /// - router: A ``RPCRouter`` used by the server to route accepted streams to method handlers.
|
|
|
+ /// - interceptors: A collection of interceptors providing cross-cutting functionality to each
|
|
|
+ /// accepted RPC. The order in which interceptors are added reflects the order in which they
|
|
|
+ /// are called. The first interceptor added will be the first interceptor to intercept each
|
|
|
+ /// request. The last interceptor added will be the final interceptor to intercept each
|
|
|
+ /// request before calling the appropriate handler.
|
|
|
+ public init(
|
|
|
+ transports: [any ServerTransport],
|
|
|
+ router: RPCRouter,
|
|
|
+ interceptors: [any ServerInterceptor] = []
|
|
|
+ ) {
|
|
|
+ self.state = ManagedAtomic(.notStarted)
|
|
|
+ self.transports = transports
|
|
|
+ self.router = router
|
|
|
+ self.interceptors = interceptors
|
|
|
}
|
|
|
|
|
|
/// Starts the server and runs until all registered transports have closed.
|
|
|
@@ -159,20 +162,19 @@ public final class GRPCServer: Sendable {
|
|
|
///
|
|
|
/// To stop the server more abruptly you can cancel the task that this function is running in.
|
|
|
///
|
|
|
- /// You must register all resources you wish to use with the server before calling this function
|
|
|
- /// as changes made after calling ``run()`` won't be reflected.
|
|
|
- ///
|
|
|
/// - Note: You can only call this function once, repeated calls will result in a
|
|
|
/// ``ServerError`` being thrown.
|
|
|
- /// - Important: You must register at least one transport by calling
|
|
|
- /// ``Transports-swift.struct/add(_:)`` before calling this method.
|
|
|
public func run() async throws {
|
|
|
- let (transports, router, interceptors) = try self.storage.withLockedValue { storage in
|
|
|
- switch storage.state {
|
|
|
+ let (wasNotStarted, actualState) = self.state.compareExchange(
|
|
|
+ expected: .notStarted,
|
|
|
+ desired: .starting,
|
|
|
+ ordering: .sequentiallyConsistent
|
|
|
+ )
|
|
|
+
|
|
|
+ guard wasNotStarted else {
|
|
|
+ switch actualState {
|
|
|
case .notStarted:
|
|
|
- storage.state = .starting
|
|
|
- return (storage.transports, storage.services.router, storage.interceptors)
|
|
|
-
|
|
|
+ fatalError()
|
|
|
case .starting, .running:
|
|
|
throw ServerError(
|
|
|
code: .serverIsAlreadyRunning,
|
|
|
@@ -189,31 +191,31 @@ public final class GRPCServer: Sendable {
|
|
|
|
|
|
// When we exit this function we must have stopped.
|
|
|
defer {
|
|
|
- self.storage.withLockedValue { $0.state = .stopped }
|
|
|
+ self.state.store(.stopped, ordering: .sequentiallyConsistent)
|
|
|
}
|
|
|
|
|
|
- if transports.values.isEmpty {
|
|
|
+ if self.transports.isEmpty {
|
|
|
throw ServerError(
|
|
|
code: .noTransportsConfigured,
|
|
|
message: """
|
|
|
Can't start server, no transports are configured. You must add at least one transport \
|
|
|
- to the server using 'transports.add(_:)' before calling 'run()'.
|
|
|
+ to the server before calling 'run()'.
|
|
|
"""
|
|
|
)
|
|
|
}
|
|
|
|
|
|
var listeners: [RPCAsyncSequence<Stream>] = []
|
|
|
- listeners.reserveCapacity(transports.values.count)
|
|
|
+ listeners.reserveCapacity(self.transports.count)
|
|
|
|
|
|
- for transport in transports.values {
|
|
|
+ for transport in self.transports {
|
|
|
do {
|
|
|
let listener = try await transport.listen()
|
|
|
listeners.append(listener)
|
|
|
} catch let cause {
|
|
|
// Failed to start, so start stopping.
|
|
|
- self.storage.withLockedValue { $0.state = .stopping }
|
|
|
+ self.state.store(.stopping, ordering: .sequentiallyConsistent)
|
|
|
// Some listeners may have started and have streams which need closing.
|
|
|
- await Self.rejectRequests(listeners, transports: transports)
|
|
|
+ await self.rejectRequests(listeners)
|
|
|
|
|
|
throw ServerError(
|
|
|
code: .failedToStartTransport,
|
|
|
@@ -227,35 +229,24 @@ public final class GRPCServer: Sendable {
|
|
|
}
|
|
|
|
|
|
// May have been told to stop listening while starting the transports.
|
|
|
- let isStopping = self.storage.withLockedValue { storage in
|
|
|
- switch storage.state {
|
|
|
- case .notStarted, .running, .stopped:
|
|
|
- fatalError("Invalid state")
|
|
|
-
|
|
|
- case .starting:
|
|
|
- storage.state = .running
|
|
|
- return false
|
|
|
-
|
|
|
- case .stopping:
|
|
|
- return true
|
|
|
- }
|
|
|
- }
|
|
|
+ let (wasStarting, _) = self.state.compareExchange(
|
|
|
+ expected: .starting,
|
|
|
+ desired: .running,
|
|
|
+ ordering: .sequentiallyConsistent
|
|
|
+ )
|
|
|
|
|
|
// If the server is stopping then notify the transport and then consume them: there may be
|
|
|
// streams opened at a lower level (e.g. HTTP/2) which are already open and need to be consumed.
|
|
|
- if isStopping {
|
|
|
- await Self.rejectRequests(listeners, transports: transports)
|
|
|
+ if wasStarting {
|
|
|
+ await self.handleRequests(listeners)
|
|
|
} else {
|
|
|
- await Self.handleRequests(listeners, router: router, interceptors: interceptors)
|
|
|
+ await self.rejectRequests(listeners)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static func rejectRequests(
|
|
|
- _ listeners: [RPCAsyncSequence<Stream>],
|
|
|
- transports: Transports
|
|
|
- ) async {
|
|
|
+ private func rejectRequests(_ listeners: [RPCAsyncSequence<Stream>]) async {
|
|
|
// Tell the active listeners to stop listening.
|
|
|
- for transport in transports.values.prefix(listeners.count) {
|
|
|
+ for transport in self.transports.prefix(listeners.count) {
|
|
|
transport.stopListening()
|
|
|
}
|
|
|
|
|
|
@@ -282,33 +273,21 @@ public final class GRPCServer: Sendable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static func handleRequests(
|
|
|
- _ listeners: [RPCAsyncSequence<Stream>],
|
|
|
- router: RPCRouter,
|
|
|
- interceptors: Interceptors
|
|
|
- ) async {
|
|
|
+ private func handleRequests(_ listeners: [RPCAsyncSequence<Stream>]) async {
|
|
|
#if swift(>=5.9)
|
|
|
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
|
|
- await Self.handleRequestsInDiscardingTaskGroup(
|
|
|
- listeners,
|
|
|
- router: router,
|
|
|
- interceptors: interceptors
|
|
|
- )
|
|
|
+ await self.handleRequestsInDiscardingTaskGroup(listeners)
|
|
|
} else {
|
|
|
- await Self.handleRequestsInTaskGroup(listeners, router: router, interceptors: interceptors)
|
|
|
+ await self.handleRequestsInTaskGroup(listeners)
|
|
|
}
|
|
|
#else
|
|
|
- await Self.handleRequestsInTaskGroup(listeners, router: router, interceptors: interceptors)
|
|
|
+ await self.handleRequestsInTaskGroup(listeners)
|
|
|
#endif
|
|
|
}
|
|
|
|
|
|
#if swift(>=5.9)
|
|
|
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
|
|
|
- private static func handleRequestsInDiscardingTaskGroup(
|
|
|
- _ listeners: [RPCAsyncSequence<Stream>],
|
|
|
- router: RPCRouter,
|
|
|
- interceptors: Interceptors
|
|
|
- ) async {
|
|
|
+ private func handleRequestsInDiscardingTaskGroup(_ listeners: [RPCAsyncSequence<Stream>]) async {
|
|
|
await withDiscardingTaskGroup { group in
|
|
|
for listener in listeners {
|
|
|
group.addTask {
|
|
|
@@ -316,7 +295,7 @@ public final class GRPCServer: Sendable {
|
|
|
do {
|
|
|
for try await stream in listener {
|
|
|
subGroup.addTask {
|
|
|
- await router.handle(stream: stream, interceptors: interceptors.values)
|
|
|
+ await self.router.handle(stream: stream, interceptors: self.interceptors)
|
|
|
}
|
|
|
}
|
|
|
} catch {
|
|
|
@@ -330,11 +309,7 @@ public final class GRPCServer: Sendable {
|
|
|
}
|
|
|
#endif
|
|
|
|
|
|
- private static func handleRequestsInTaskGroup(
|
|
|
- _ listeners: [RPCAsyncSequence<Stream>],
|
|
|
- router: RPCRouter,
|
|
|
- interceptors: Interceptors
|
|
|
- ) async {
|
|
|
+ private func handleRequestsInTaskGroup(_ listeners: [RPCAsyncSequence<Stream>]) async {
|
|
|
// If the discarding task group isn't available then fall back to using a regular task group
|
|
|
// with a limit on subtasks. Most servers will use an HTTP/2 based transport, most
|
|
|
// implementations limit connections to 100 concurrent streams. A limit of 4096 gives the server
|
|
|
@@ -355,7 +330,7 @@ public final class GRPCServer: Sendable {
|
|
|
}
|
|
|
|
|
|
subGroup.addTask {
|
|
|
- await router.handle(stream: stream, interceptors: interceptors.values)
|
|
|
+ await self.router.handle(stream: stream, interceptors: self.interceptors)
|
|
|
}
|
|
|
}
|
|
|
} catch {
|
|
|
@@ -375,105 +350,50 @@ public final class GRPCServer: Sendable {
|
|
|
///
|
|
|
/// Calling this on a server which is already stopping or has stopped has no effect.
|
|
|
public func stopListening() {
|
|
|
- let transports = self.storage.withLockedValue { storage in
|
|
|
- let transports: Transports?
|
|
|
-
|
|
|
- switch storage.state {
|
|
|
- case .notStarted:
|
|
|
- storage.state = .stopped
|
|
|
- transports = nil
|
|
|
- case .starting:
|
|
|
- storage.state = .stopping
|
|
|
- transports = nil
|
|
|
- case .running:
|
|
|
- storage.state = .stopping
|
|
|
- transports = storage.transports
|
|
|
- case .stopping:
|
|
|
- transports = nil
|
|
|
- case .stopped:
|
|
|
- transports = nil
|
|
|
- }
|
|
|
-
|
|
|
- return transports
|
|
|
- }
|
|
|
-
|
|
|
- if let transports = transports?.values {
|
|
|
- for transport in transports {
|
|
|
+ let (wasRunning, actual) = self.state.compareExchange(
|
|
|
+ expected: .running,
|
|
|
+ desired: .stopping,
|
|
|
+ ordering: .sequentiallyConsistent
|
|
|
+ )
|
|
|
+
|
|
|
+ if wasRunning {
|
|
|
+ for transport in self.transports {
|
|
|
transport.stopListening()
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
|
|
-extension GRPCServer {
|
|
|
- /// The transports which provide a bidirectional communication channel with clients.
|
|
|
- ///
|
|
|
- /// You can add a new transport by calling ``add(_:)``.
|
|
|
- public struct Transports: Sendable {
|
|
|
- private(set) var values: [any (ServerTransport & Sendable)] = []
|
|
|
-
|
|
|
- /// Add a transport to the server.
|
|
|
- ///
|
|
|
- /// - Parameter transport: The transport to add.
|
|
|
- public mutating func add(_ transport: some (ServerTransport & Sendable)) {
|
|
|
- self.values.append(transport)
|
|
|
- }
|
|
|
- }
|
|
|
+ } else {
|
|
|
+ switch actual {
|
|
|
+ case .notStarted:
|
|
|
+ let (exchanged, _) = self.state.compareExchange(
|
|
|
+ expected: .notStarted,
|
|
|
+ desired: .stopped,
|
|
|
+ ordering: .sequentiallyConsistent
|
|
|
+ )
|
|
|
|
|
|
- /// The services registered with this server.
|
|
|
- ///
|
|
|
- /// You can register services by calling ``register(_:)`` or by manually adding handlers for
|
|
|
- /// methods to the ``router``.
|
|
|
- public struct Services: Sendable {
|
|
|
- /// The router storing handlers for known methods.
|
|
|
- public var router = RPCRouter()
|
|
|
-
|
|
|
- /// Registers service methods with the ``router``.
|
|
|
- ///
|
|
|
- /// - Parameter service: The service to register with the ``router``.
|
|
|
- public mutating func register(_ service: some RegistrableRPCService) {
|
|
|
- service.registerMethods(with: &self.router)
|
|
|
- }
|
|
|
- }
|
|
|
+ // Lost a race with 'run()', try again.
|
|
|
+ if !exchanged {
|
|
|
+ self.stopListening()
|
|
|
+ }
|
|
|
|
|
|
- /// A collection of interceptors providing cross-cutting functionality to each accepted RPC.
|
|
|
- public struct Interceptors: Sendable {
|
|
|
- private(set) var values: [any ServerInterceptor] = []
|
|
|
-
|
|
|
- /// Add an interceptor to the server.
|
|
|
- ///
|
|
|
- /// The order in which interceptors are added reflects the order in which they are called. The
|
|
|
- /// first interceptor added will be the first interceptor to intercept each request. The last
|
|
|
- /// interceptor added will be the final interceptor to intercept each request before calling
|
|
|
- /// the appropriate handler.
|
|
|
- ///
|
|
|
- /// - Parameter interceptor: The interceptor to add.
|
|
|
- public mutating func add(_ interceptor: some ServerInterceptor) {
|
|
|
- self.values.append(interceptor)
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+ case .starting:
|
|
|
+ let (exchanged, _) = self.state.compareExchange(
|
|
|
+ expected: .starting,
|
|
|
+ desired: .stopping,
|
|
|
+ ordering: .sequentiallyConsistent
|
|
|
+ )
|
|
|
|
|
|
-@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
|
|
-extension GRPCServer.Transports: CustomStringConvertible {
|
|
|
- public var description: String {
|
|
|
- return String(describing: self.values)
|
|
|
- }
|
|
|
-}
|
|
|
+ // Lost a race with 'run()', try again.
|
|
|
+ if !exchanged {
|
|
|
+ self.stopListening()
|
|
|
+ }
|
|
|
|
|
|
-@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
|
|
-extension GRPCServer.Services: CustomStringConvertible {
|
|
|
- public var description: String {
|
|
|
- // List the fully qualified all methods ordered by service and then method
|
|
|
- let rpcs = self.router.methods.map { $0.fullyQualifiedMethod }.sorted()
|
|
|
- return String(describing: rpcs)
|
|
|
- }
|
|
|
-}
|
|
|
+ case .running:
|
|
|
+ // Unreachable, this branch only happens when the initial exchange didn't take place.
|
|
|
+ fatalError()
|
|
|
|
|
|
-@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
|
|
-extension GRPCServer.Interceptors: CustomStringConvertible {
|
|
|
- public var description: String {
|
|
|
- return String(describing: self.values.map { String(describing: type(of: $0)) })
|
|
|
+ case .stopping, .stopped:
|
|
|
+ // Already stopping/stopped, ignore.
|
|
|
+ ()
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|