Skip to content

Expose APIs to customise NWParameters when using NIOTS #2223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 25, 2025
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