Skip to content
18 changes: 18 additions & 0 deletions Sources/GRPC/ClientConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,24 @@ extension ClientConnection {
@preconcurrency
public var debugChannelInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)?

#if canImport(Network)
@available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *)
public var clientBootstrapNWParametersConfigurator: (
@Sendable (NIOTSConnectionBootstrap) -> Void
)? {
get {
return self._clientBootstrapNWParametersConfigurator as! (
@Sendable (NIOTSConnectionBootstrap) -> Void
)?
}
set {
self._clientBootstrapNWParametersConfigurator = newValue
}
}

private var _clientBootstrapNWParametersConfigurator: (any Sendable)?
#endif

#if canImport(NIOSSL)
/// Create a `Configuration` with some pre-defined defaults. Prefer using
/// `ClientConnection.secure(group:)` to build a connection secured with TLS or
Expand Down
64 changes: 64 additions & 0 deletions Sources/GRPC/ConnectionManagerChannelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,56 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
@usableFromInline
internal var debugChannelInitializer: Optional<(Channel) -> EventLoopFuture<Void>>

#if canImport(Network)
@available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *)
@usableFromInline
internal var clientBootstrapNWParametersConfigurator: (
@Sendable (NIOTSConnectionBootstrap) -> Void
)? {
get {
return self._clientBootstrapNWParametersConfigurator as! (
@Sendable (NIOTSConnectionBootstrap) -> Void
)?
}
set {
self._clientBootstrapNWParametersConfigurator = newValue
}
}

private var _clientBootstrapNWParametersConfigurator: (any Sendable)?
#endif

#if canImport(Network)
@inlinable
@available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *)
internal init(
connectionTarget: ConnectionTarget,
connectionKeepalive: ClientConnectionKeepalive,
connectionIdleTimeout: TimeAmount,
tlsMode: TLSMode,
tlsConfiguration: GRPCTLSConfiguration?,
httpTargetWindowSize: Int,
httpMaxFrameSize: Int,
errorDelegate: ClientErrorDelegate?,
debugChannelInitializer: ((Channel) -> EventLoopFuture<Void>)?,
clientBootstrapNWParametersConfigurator: (@Sendable (NIOTSConnectionBootstrap) -> Void)?
) {
self.init(
connectionTarget: connectionTarget,
connectionKeepalive: connectionKeepalive,
connectionIdleTimeout: connectionIdleTimeout,
tlsMode: tlsMode,
tlsConfiguration: tlsConfiguration,
httpTargetWindowSize: httpTargetWindowSize,
httpMaxFrameSize: httpMaxFrameSize,
errorDelegate: errorDelegate,
debugChannelInitializer: debugChannelInitializer
)

self.clientBootstrapNWParametersConfigurator = clientBootstrapNWParametersConfigurator
}
#endif

@inlinable
internal init(
connectionTarget: ConnectionTarget,
Expand Down Expand Up @@ -133,6 +183,12 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
errorDelegate: configuration.errorDelegate,
debugChannelInitializer: configuration.debugChannelInitializer
)

#if canImport(Network)
if #available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) {
self.clientBootstrapNWParametersConfigurator = configuration.clientBootstrapNWParametersConfigurator
}
#endif
}

private var serverHostname: String? {
Expand Down Expand Up @@ -210,6 +266,14 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
return channel.eventLoop.makeFailedFuture(error)
}

#if canImport(Network)
if #available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *),
let configurator = self.clientBootstrapNWParametersConfigurator,
let niotsBootstrap = bootstrap as? NIOTSConnectionBootstrap {
configurator(niotsBootstrap)
}
#endif

// Run the debug initializer, if there is one.
if let debugInitializer = self.debugChannelInitializer {
return debugInitializer(channel)
Expand Down
39 changes: 39 additions & 0 deletions Sources/GRPC/ConnectionPool/GRPCChannelPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import Logging
import NIOCore
import NIOPosix

#if canImport(Network)
import NIOTransportServices
#endif

import struct Foundation.UUID

public enum GRPCChannelPool {
Expand Down Expand Up @@ -191,6 +195,8 @@ extension GRPCChannelPool {
return SwiftLogNoOpLogHandler()
}
)

public var transportServices: TransportServices = .defaults
}
}

Expand Down Expand Up @@ -299,6 +305,39 @@ extension GRPCChannelPool.Configuration {
}
}

#if canImport(Network)
extension GRPCChannelPool.Configuration {
public struct TransportServices: Sendable {
/// Default transport services configuration.
public static let defaults = Self()

@inlinable
public static func with(_ configure: (inout Self) -> Void) -> Self {
var configuration = Self.defaults
configure(&configuration)
return configuration
}

// TODO: write docs
@available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *)
public var clientBootstrapNWParametersConfigurator: (
@Sendable (NIOTSConnectionBootstrap) -> Void
)? {
get {
return self._clientBootstrapNWParametersConfigurator as! (
@Sendable (NIOTSConnectionBootstrap) -> Void
)?
}
set {
self._clientBootstrapNWParametersConfigurator = newValue
}
}

private var _clientBootstrapNWParametersConfigurator: (any Sendable)?
}
}
#endif // canImport(Network)

/// The ID of a connection in the connection pool.
public struct GRPCConnectionID: Hashable, Sendable, CustomStringConvertible {
private enum Value: Sendable, Hashable {
Expand Down
32 changes: 31 additions & 1 deletion Sources/GRPC/ConnectionPool/PooledChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,36 @@ internal final class PooledChannel: GRPCChannel {

self._scheme = scheme

let provider = DefaultChannelProvider(
let provider: DefaultChannelProvider
#if canImport(Network)
if #available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) {
provider = DefaultChannelProvider(
connectionTarget: configuration.target,
connectionKeepalive: configuration.keepalive,
connectionIdleTimeout: configuration.idleTimeout,
tlsMode: tlsMode,
tlsConfiguration: configuration.transportSecurity.tlsConfiguration,
httpTargetWindowSize: configuration.http2.targetWindowSize,
httpMaxFrameSize: configuration.http2.maxFrameSize,
errorDelegate: configuration.errorDelegate,
debugChannelInitializer: configuration.debugChannelInitializer,
clientBootstrapNWParametersConfigurator: configuration.transportServices.clientBootstrapNWParametersConfigurator
)
} else {
provider = DefaultChannelProvider(
connectionTarget: configuration.target,
connectionKeepalive: configuration.keepalive,
connectionIdleTimeout: configuration.idleTimeout,
tlsMode: tlsMode,
tlsConfiguration: configuration.transportSecurity.tlsConfiguration,
httpTargetWindowSize: configuration.http2.targetWindowSize,
httpMaxFrameSize: configuration.http2.maxFrameSize,
errorDelegate: configuration.errorDelegate,
debugChannelInitializer: configuration.debugChannelInitializer
)
}
#else
provider = DefaultChannelProvider(
connectionTarget: configuration.target,
connectionKeepalive: configuration.keepalive,
connectionIdleTimeout: configuration.idleTimeout,
Expand All @@ -90,6 +119,7 @@ internal final class PooledChannel: GRPCChannel {
errorDelegate: configuration.errorDelegate,
debugChannelInitializer: configuration.debugChannelInitializer
)
#endif

self._pool = PoolManager.makeInitializedPoolManager(
using: configuration.eventLoopGroup,
Expand Down
24 changes: 24 additions & 0 deletions Sources/GRPC/Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ public final class Server: @unchecked Sendable {
_ = transportServicesBootstrap.tlsOptions(from: tlsConfiguration)
}
}

if #available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *),
let nwParametersConfigurator = configuration.serverBootstrapNWParametersConfigurator,
let transportServicesBootstrap = bootstrap as? NIOTSListenerBootstrap {
nwParametersConfigurator(transportServicesBootstrap)
}
#endif // canImport(Network)

return
Expand Down Expand Up @@ -384,6 +390,24 @@ extension Server {
/// the need to recalculate this dictionary each time we receive an rpc.
internal var serviceProvidersByName: [Substring: CallHandlerProvider]

#if canImport(Network)
@available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *)
public var serverBootstrapNWParametersConfigurator: (
@Sendable (NIOTSListenerBootstrap) -> Void
)? {
get {
return self._serverBootstrapNWParametersConfigurator as! (
@Sendable (NIOTSListenerBootstrap) -> Void
)?
}
set {
self._serverBootstrapNWParametersConfigurator = newValue
}
}

private var _serverBootstrapNWParametersConfigurator: (any Sendable)?
#endif

/// CORS configuration for gRPC-Web support.
public var webCORS = Configuration.CORS()

Expand Down
54 changes: 54 additions & 0 deletions Tests/GRPCTests/ConnectionManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ import NIOEmbedded
import NIOHTTP2
import XCTest

#if canImport(Network)
import NIOConcurrencyHelpers
import NIOTransportServices
import Network
#endif

@testable import GRPC

class ConnectionManagerTests: GRPCTestCase {
Expand Down Expand Up @@ -1412,6 +1418,54 @@ extension ConnectionManagerTests {
XCTAssert(error is DoomedChannelError)
}
}

#if canImport(Network)
func testDefaultChannelProvider_NWParametersConfigurator() throws {
let counter = NIOLockedValueBox(0)
let group = NIOTSEventLoopGroup(loopCount: 1)
var configuration = ClientConnection.Configuration.default(
target: .unixDomainSocket("/ignored"),
eventLoopGroup: group
)
configuration.clientBootstrapNWParametersConfigurator = { _ in
counter.withLockedValue { $0 += 1 }
}

// We need to trigger the connection to apply the parameters configurator.
// However, we don't actually care about establishing the connection: only triggering it.
// In other tests, we used mocked channel providers to simply return completed futures when "creating"
// the channels. However, in this test we want to make sure that the `DefaultChannelProvider`,
// which is the one we actually use, correctly sets and executes the configurator.
// For this reason, we can wait on a promise that is succeeded after the configurator has been called,
// in the debug channel initializer. This promise will always succeed regardless of the actual
// connection being established. And because this closure is executed after the parameters configurator,
// we know the counter should be updated by the time the promise has been completed.
let promise = group.next().makePromise(of: Void.self)
configuration.debugChannelInitializer = { channel in
promise.succeed()
return promise.futureResult
}

let manager = ConnectionManager(
configuration: configuration,
connectivityDelegate: self.monitor,
idleBehavior: .closeWhenIdleTimeout,
logger: self.logger
)

// Start connecting. We don't care about the actual result of the connection.
_ = manager.getHTTP2Multiplexer()

// Wait until the configurator has been called.
try promise.futureResult.wait()

XCTAssertEqual(1, counter.withLockedValue({ $0 }))

try group.syncShutdownGracefully()
}
#endif

// TODO: add test using the default channel provider that uses the parameter configurator and make sure it gets called
}

internal struct Change: Hashable, CustomStringConvertible {
Expand Down
26 changes: 25 additions & 1 deletion Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ import NIOPosix
import NIOSSL
import XCTest

#if canImport(Network)
import Network
import NIOTransportServices
#endif

final class GRPCChannelPoolTests: GRPCTestCase {
private var group: MultiThreadedEventLoopGroup!
private var group: (any EventLoopGroup)!
private var server: Server?
private var channel: GRPCChannel?

Expand Down Expand Up @@ -618,5 +623,24 @@ final class GRPCChannelPoolTests: GRPCTestCase {

XCTAssertGreaterThan(statsEvents.count, 0)
}

#if canImport(Network)
func testNWParametersConfigurator() {
let counter = NIOLockedValueBox(0)
self.group = NIOTSEventLoopGroup()
self.startServer(withTLS: false)
self.startChannel(withTLS: false) { configuration in
configuration.transportServices.clientBootstrapNWParametersConfigurator = { _ in
counter.withLockedValue { $0 += 1 }
}
}

// Execute an RPC to make sure a channel gets created/activated and the parameters configurator run.
let rpc = self.echo.get(.with { $0.text = "" })
XCTAssertNoThrow(try rpc.status.wait())

XCTAssertEqual(1, counter.withLockedValue({ $0 }))
}
#endif // canImport(Network)
}
#endif // canImport(NIOSSL)
Loading
Loading