|
|
@@ -30,6 +30,111 @@ extension HTTP2ServerTransport {
|
|
|
private let eventLoopGroup: NIOTSEventLoopGroup
|
|
|
private let serverQuiescingHelper: ServerQuiescingHelper
|
|
|
|
|
|
+ private enum State {
|
|
|
+ case idle(EventLoopPromise<GRPCHTTP2Core.SocketAddress>)
|
|
|
+ case listening(EventLoopFuture<GRPCHTTP2Core.SocketAddress>)
|
|
|
+ case closedOrInvalidAddress(RuntimeError)
|
|
|
+
|
|
|
+ var listeningAddressFuture: EventLoopFuture<GRPCHTTP2Core.SocketAddress> {
|
|
|
+ get throws {
|
|
|
+ switch self {
|
|
|
+ case .idle(let eventLoopPromise):
|
|
|
+ return eventLoopPromise.futureResult
|
|
|
+ case .listening(let eventLoopFuture):
|
|
|
+ return eventLoopFuture
|
|
|
+ case .closedOrInvalidAddress(let runtimeError):
|
|
|
+ throw runtimeError
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ enum OnBound {
|
|
|
+ case succeedPromise(
|
|
|
+ _ promise: EventLoopPromise<GRPCHTTP2Core.SocketAddress>,
|
|
|
+ address: GRPCHTTP2Core.SocketAddress
|
|
|
+ )
|
|
|
+ case failPromise(
|
|
|
+ _ promise: EventLoopPromise<GRPCHTTP2Core.SocketAddress>,
|
|
|
+ error: RuntimeError
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ mutating func addressBound(_ address: NIOCore.SocketAddress?) -> OnBound {
|
|
|
+ switch self {
|
|
|
+ case .idle(let listeningAddressPromise):
|
|
|
+ if let address {
|
|
|
+ self = .listening(listeningAddressPromise.futureResult)
|
|
|
+ return .succeedPromise(
|
|
|
+ listeningAddressPromise,
|
|
|
+ address: GRPCHTTP2Core.SocketAddress(address)
|
|
|
+ )
|
|
|
+
|
|
|
+ } else {
|
|
|
+ assertionFailure("Unknown address type")
|
|
|
+ let invalidAddressError = RuntimeError(
|
|
|
+ code: .transportError,
|
|
|
+ message: "Unknown address type returned by transport."
|
|
|
+ )
|
|
|
+ self = .closedOrInvalidAddress(invalidAddressError)
|
|
|
+ return .failPromise(listeningAddressPromise, error: invalidAddressError)
|
|
|
+ }
|
|
|
+
|
|
|
+ case .listening, .closedOrInvalidAddress:
|
|
|
+ fatalError(
|
|
|
+ "Invalid state: addressBound should only be called once and when in idle state"
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ enum OnClose {
|
|
|
+ case failPromise(
|
|
|
+ EventLoopPromise<GRPCHTTP2Core.SocketAddress>,
|
|
|
+ error: RuntimeError
|
|
|
+ )
|
|
|
+ case doNothing
|
|
|
+ }
|
|
|
+
|
|
|
+ mutating func close() -> OnClose {
|
|
|
+ let serverStoppedError = RuntimeError(
|
|
|
+ code: .serverIsStopped,
|
|
|
+ message: """
|
|
|
+ There is no listening address bound for this server: there may have been \
|
|
|
+ an error which caused the transport to close, or it may have shut down.
|
|
|
+ """
|
|
|
+ )
|
|
|
+
|
|
|
+ switch self {
|
|
|
+ case .idle(let listeningAddressPromise):
|
|
|
+ self = .closedOrInvalidAddress(serverStoppedError)
|
|
|
+ return .failPromise(listeningAddressPromise, error: serverStoppedError)
|
|
|
+
|
|
|
+ case .listening:
|
|
|
+ self = .closedOrInvalidAddress(serverStoppedError)
|
|
|
+ return .doNothing
|
|
|
+
|
|
|
+ case .closedOrInvalidAddress:
|
|
|
+ return .doNothing
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private let listeningAddressState: _LockedValueBox<State>
|
|
|
+
|
|
|
+ /// The listening address for this server transport.
|
|
|
+ ///
|
|
|
+ /// It is an `async` property because it will only return once the address has been successfully bound.
|
|
|
+ ///
|
|
|
+ /// - Throws: A runtime error will be thrown if the address could not be bound or is not bound any
|
|
|
+ /// longer, because the transport isn't listening anymore. It can also throw if the transport returned an
|
|
|
+ /// invalid address.
|
|
|
+ public var listeningAddress: GRPCHTTP2Core.SocketAddress {
|
|
|
+ get async throws {
|
|
|
+ try await self.listeningAddressState
|
|
|
+ .withLockedValue { try $0.listeningAddressFuture }
|
|
|
+ .get()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/// Create a new `TransportServices` transport.
|
|
|
///
|
|
|
/// - Parameters:
|
|
|
@@ -45,11 +150,23 @@ extension HTTP2ServerTransport {
|
|
|
self.config = config
|
|
|
self.eventLoopGroup = eventLoopGroup
|
|
|
self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup)
|
|
|
+
|
|
|
+ let eventLoop = eventLoopGroup.any()
|
|
|
+ self.listeningAddressState = _LockedValueBox(.idle(eventLoop.makePromise()))
|
|
|
}
|
|
|
|
|
|
public func listen(
|
|
|
_ streamHandler: @escaping (RPCStream<Inbound, Outbound>) async -> Void
|
|
|
) async throws {
|
|
|
+ defer {
|
|
|
+ switch self.listeningAddressState.withLockedValue({ $0.close() }) {
|
|
|
+ case .failPromise(let promise, let error):
|
|
|
+ promise.fail(error)
|
|
|
+ case .doNothing:
|
|
|
+ ()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
let serverChannel = try await NIOTSListenerBootstrap(group: self.eventLoopGroup)
|
|
|
.serverChannelInitializer { channel in
|
|
|
let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler(
|
|
|
@@ -70,6 +187,16 @@ extension HTTP2ServerTransport {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ let action = self.listeningAddressState.withLockedValue {
|
|
|
+ $0.addressBound(serverChannel.channel.localAddress)
|
|
|
+ }
|
|
|
+ switch action {
|
|
|
+ case .succeedPromise(let promise, let address):
|
|
|
+ promise.succeed(address)
|
|
|
+ case .failPromise(let promise, let error):
|
|
|
+ promise.fail(error)
|
|
|
+ }
|
|
|
+
|
|
|
try await serverChannel.executeThenClose { inbound in
|
|
|
try await withThrowingDiscardingTaskGroup { serverTaskGroup in
|
|
|
for try await (connectionChannel, streamMultiplexer) in inbound {
|
|
|
@@ -203,7 +330,7 @@ extension NIOTSListenerBootstrap {
|
|
|
to address: GRPCHTTP2Core.SocketAddress,
|
|
|
childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<Output>
|
|
|
) async throws -> NIOAsyncChannel<Output, Never> {
|
|
|
- if let virtualSocket = address.virtualSocket {
|
|
|
+ if address.virtualSocket != nil {
|
|
|
throw RuntimeError(
|
|
|
code: .transportError,
|
|
|
message: """
|