Skip to content

Commit 2a1fa74

Browse files
author
John Kassebaum
committed
Make HTTP/2 flow control target window size configurable in a client connection.
Motivation: Higher stremaing thruput from server to client is available by specifying a flow control window size higher than the initial value of 2^16-1. The target value needs to be passed either using SETTINGS_INITIAL_WINDOW_SIZE in the connection preface, or via a WINDOW_UPDATE. The HTTP2FlowControlWindow passes the value configured here in `windowUpdate`. Modifications: Added property `targetWindowSize` to `ClientConnection.Configuration`. In the existing `ClientConnection.Configuration.init`, sets the new `targetWindowSize` property to the initial flow-control window size (65535). Duplicated `ClientConnection.Configuration.init` and added a `targetWindowSize` arg. Added property and setter for `targetWindowSize` to `ClientConnection.Builder`. During client bootstrap, pass `configuration.targetWindowSize` to the channel initializer. Duplicated channel initializer `configureGRPCClient` and added a `targetWindowSize` arg. Duplicated all tests that directly invoke the original `ClientConnection.Configuration.init` to explicitly pass `targetWindowSize`to the new initializer. Result: One can build a `ClientConnection` with a target window size value that is configured via a WINDOW_UPDATE after the connection is established.
1 parent 1b18470 commit 2a1fa74

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed

Sources/GRPC/ClientConnection.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ extension ClientConnection {
419419
.channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
420420
.channelInitializer { channel in
421421
channel.configureGRPCClient(
422+
targetWindowSize: configuration.targetWindowSize,
422423
tlsConfiguration: configuration.tls?.configuration,
423424
tlsServerHostname: serverHostname,
424425
connectivityMonitor: connectivityMonitor,
@@ -467,6 +468,9 @@ extension ClientConnection {
467468
public struct Configuration {
468469
/// The target to connect to.
469470
public var target: ConnectionTarget
471+
472+
///The HTTP/2 flow control target window size.
473+
public var targetWindowSize: Int
470474

471475
/// The event loop group to run the connection on.
472476
public var eventLoopGroup: EventLoopGroup
@@ -512,6 +516,38 @@ extension ClientConnection {
512516
connectionBackoff: ConnectionBackoff? = ConnectionBackoff()
513517
) {
514518
self.target = target
519+
self.targetWindowSize = 65535
520+
self.eventLoopGroup = eventLoopGroup
521+
self.errorDelegate = errorDelegate
522+
self.connectivityStateDelegate = connectivityStateDelegate
523+
self.tls = tls
524+
self.connectionBackoff = connectionBackoff
525+
}
526+
527+
/// Create a `Configuration` with some pre-defined defaults. Prefer using
528+
/// `ClientConnection.secure(group:)` to build a connection secured with TLS or
529+
/// `ClientConnection.insecure(group:)` to build a plaintext connection.
530+
///
531+
/// - Parameter target: The target to connect to.
532+
/// - Parameter targetWindowSize: The HTTP/2 flow control target window size.
533+
/// - Parameter eventLoopGroup: The event loop group to run the connection on.
534+
/// - Parameter errorDelegate: The error delegate, defaulting to a delegate which will log only
535+
/// on debug builds.
536+
/// - Parameter connectivityStateDelegate: A connectivity state delegate, defaulting to `nil`.
537+
/// - Parameter tlsConfiguration: TLS configuration, defaulting to `nil`.
538+
/// - Parameter connectionBackoff: The connection backoff configuration to use.
539+
/// - Parameter messageEncoding: Message compression configuration, defaults to no compression.
540+
public init(
541+
target: ConnectionTarget,
542+
targetWindowSize: Int,
543+
eventLoopGroup: EventLoopGroup,
544+
errorDelegate: ClientErrorDelegate? = LoggingClientErrorDelegate(),
545+
connectivityStateDelegate: ConnectivityStateDelegate? = nil,
546+
tls: Configuration.TLS? = nil,
547+
connectionBackoff: ConnectionBackoff? = ConnectionBackoff()
548+
) {
549+
self.target = target
550+
self.targetWindowSize = targetWindowSize
515551
self.eventLoopGroup = eventLoopGroup
516552
self.errorDelegate = errorDelegate
517553
self.connectivityStateDelegate = connectivityStateDelegate
@@ -601,6 +637,33 @@ extension Channel {
601637
}
602638
}
603639

640+
func configureGRPCClient(
641+
targetWindowSize: Int,
642+
tlsConfiguration: TLSConfiguration?,
643+
tlsServerHostname: String?,
644+
connectivityMonitor: ConnectivityStateMonitor,
645+
errorDelegate: ClientErrorDelegate?,
646+
logger: Logger
647+
) -> EventLoopFuture<Void> {
648+
let tlsConfigured = tlsConfiguration.map {
649+
self.configureTLS($0, serverHostname: tlsServerHostname, errorDelegate: errorDelegate, logger: logger)
650+
}
651+
652+
return (tlsConfigured ?? self.eventLoop.makeSucceededFuture(())).flatMap {
653+
self.configureHTTP2Pipeline(mode: .client, targetWindowSize: targetWindowSize)
654+
}.flatMap { _ in
655+
let settingsObserver = InitialSettingsObservingHandler(
656+
connectivityStateMonitor: connectivityMonitor,
657+
logger: logger
658+
)
659+
let errorHandler = DelegatingErrorHandler(
660+
logger: logger,
661+
delegate: errorDelegate
662+
)
663+
return self.pipeline.addHandlers(settingsObserver, errorHandler)
664+
}
665+
}
666+
604667
func configureGRPCClient(
605668
errorDelegate: ClientErrorDelegate?,
606669
logger: Logger

Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ extension ClientConnection {
3131
extension ClientConnection {
3232
public class Builder {
3333
private let group: EventLoopGroup
34+
private var targetWindowSize: Int = 65535
3435
private var maybeTLS: ClientConnection.Configuration.TLS? { return nil }
3536
private var connectionBackoff = ConnectionBackoff()
3637
private var connectionBackoffIsEnabled = true
@@ -44,6 +45,7 @@ extension ClientConnection {
4445
public func connect(host: String, port: Int) -> ClientConnection {
4546
let configuration = ClientConnection.Configuration(
4647
target: .hostAndPort(host, port),
48+
targetWindowSize: self.targetWindowSize,
4749
eventLoopGroup: self.group,
4850
errorDelegate: self.errorDelegate,
4951
connectivityStateDelegate: self.connectivityStateDelegate,
@@ -132,6 +134,15 @@ extension ClientConnection.Builder {
132134
}
133135
}
134136

137+
extension ClientConnection.Builder {
138+
/// Sets the HTTP/2 flow control target window size.
139+
@discardableResult
140+
public func withTargetWindowSize(_ targetWindowSize: Int) -> Self {
141+
self.targetWindowSize = targetWindowSize
142+
return self
143+
}
144+
}
145+
135146
extension ClientConnection.Builder {
136147
/// Sets the client error delegate.
137148
@discardableResult

Tests/GRPCTests/ClientTLSFailureTests.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ class ClientTLSFailureTests: GRPCTestCase {
6565
)
6666
}
6767

68+
func makeClientConfiguration(
69+
tls: ClientConnection.Configuration.TLS,
70+
targetWindowSize: Int
71+
) -> ClientConnection.Configuration {
72+
return .init(
73+
target: .hostAndPort("localhost", self.port),
74+
targetWindowSize: targetWindowSize,
75+
eventLoopGroup: self.clientEventLoopGroup,
76+
tls: tls,
77+
// No need to retry connecting.
78+
connectionBackoff: nil
79+
)
80+
}
81+
6882
func makeClientConnectionExpectation() -> XCTestExpectation {
6983
return self.expectation(description: "EventLoopFuture<ClientConnection> resolved")
7084
}
@@ -123,6 +137,32 @@ class ClientTLSFailureTests: GRPCTestCase {
123137
XCTFail("Expected NIOSSLError.handshakeFailed(BoringSSL.sslError)")
124138
}
125139
}
140+
141+
func testClientConnectionFailsWhenServerIsUnknownWithTargetWindowSize() throws {
142+
let shutdownExpectation = self.expectation(description: "client shutdown")
143+
let errorExpectation = self.expectation(description: "error")
144+
145+
var tls = self.defaultClientTLSConfiguration
146+
tls.trustRoots = .certificates([])
147+
var configuration = self.makeClientConfiguration(tls: tls, targetWindowSize: 65535)
148+
149+
let errorRecorder = ErrorRecordingDelegate(expectation: errorExpectation)
150+
configuration.errorDelegate = errorRecorder
151+
152+
let stateChangeDelegate = ConnectivityStateCollectionDelegate(shutdown: shutdownExpectation)
153+
configuration.connectivityStateDelegate = stateChangeDelegate
154+
155+
_ = ClientConnection(configuration: configuration)
156+
157+
self.wait(for: [shutdownExpectation, errorExpectation], timeout: self.defaultTestTimeout)
158+
159+
if let nioSSLError = errorRecorder.errors.first as? NIOSSLError,
160+
case .handshakeFailed(.sslError) = nioSSLError {
161+
// Expected case.
162+
} else {
163+
XCTFail("Expected NIOSSLError.handshakeFailed(BoringSSL.sslError)")
164+
}
165+
}
126166

127167
func testClientConnectionFailsWhenHostnameIsNotValid() throws {
128168
let shutdownExpectation = self.expectation(description: "client shutdown")
@@ -149,4 +189,30 @@ class ClientTLSFailureTests: GRPCTestCase {
149189
XCTFail("Expected NIOSSLExtraError.failedToValidateHostname")
150190
}
151191
}
192+
193+
func testClientConnectionFailsWhenHostnameIsNotValidWithTargetWindowSize() throws {
194+
let shutdownExpectation = self.expectation(description: "client shutdown")
195+
let errorExpectation = self.expectation(description: "error")
196+
197+
var tls = self.defaultClientTLSConfiguration
198+
tls.hostnameOverride = "not-the-server-hostname"
199+
200+
var configuration = self.makeClientConfiguration(tls: tls, targetWindowSize: 65535)
201+
let errorRecorder = ErrorRecordingDelegate(expectation: errorExpectation)
202+
configuration.errorDelegate = errorRecorder
203+
204+
let stateChangeDelegate = ConnectivityStateCollectionDelegate(shutdown: shutdownExpectation)
205+
configuration.connectivityStateDelegate = stateChangeDelegate
206+
207+
let _ = ClientConnection(configuration: configuration)
208+
209+
self.wait(for: [shutdownExpectation, errorExpectation], timeout: self.defaultTestTimeout)
210+
211+
if let nioSSLError = errorRecorder.errors.first as? NIOSSLExtraError {
212+
XCTAssertEqual(nioSSLError, .failedToValidateHostname)
213+
// Expected case.
214+
} else {
215+
XCTFail("Expected NIOSSLExtraError.failedToValidateHostname")
216+
}
217+
}
152218
}

0 commit comments

Comments
 (0)