Browse Source

Make HTTP/2 flow control window size configurable for clients and servers (#786)

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:

Set package and podspec dependency swift-nio-http2 target to 1.12.0, needed for pipeline configuration with http/2 flow control target window size coniguration.
Added property `httpTargetWindowSize` to `ClientConnection.Configuration`.
In `ClientConnection.Configuration.init`, added httpTargetWindowSize arg with default value 2^16-1.
Added property and setter for `httpTargetWindowSize` to `ClientConnection.Builder`.
During client bootstrap in ConnectionManager, pass `configuration.httpTargetWindowSize` to the channel configurator.
Configure http/2 flow control target window size on the grpc client pipeline.
Duplicated all tests that directly invoke the original `ClientConnection.Configuration.init` to explicitly pass `httpTargetWindowSize` to the new initializer.

Result:

One can build a `ClientConnection` with a http/2 flow control target window size value that is configured via a WINDOW_UPDATE after the connection is established.

* Make HTTP/2 flow control target window size configurable in a server connection.

Motivation:

Higher streaming thruput from client to server 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 `httpTargetWindowSize` to `Server.Configuration`.
In `Server.Configuration.init`, added httpTargetWindowSize arg with default value 2^16-1.
Added property and setter for `httpTargetWindowSize` to `Server.Builder`.
During server bootstrap in Server.start, pass `configuration.httpTargetWindowSize` to the HTTPProtocolSwitcher.
Configure http/2 flow control target window size on the http2 pipeline in HTTPProxySwitcher.

Result:

One can build a `Server` with a target window size value that is configured via a WINDOW_UPDATE after the connection is established.
John Kassebaum 5 years ago
parent
commit
f71049eb20

+ 2 - 2
Package.resolved

@@ -24,8 +24,8 @@
         "repositoryURL": "https://github.com/apple/swift-nio-http2.git",
         "state": {
           "branch": null,
-          "revision": "82eb3fa0974b838358ee46bc6c5381e5ae5de6b9",
-          "version": "1.11.0"
+          "revision": "c8f952dbc37fe60def17eb15e2c90787ce6ee78a",
+          "version": "1.12.1"
         }
       },
       {

+ 1 - 1
Package.swift

@@ -29,7 +29,7 @@ let package = Package(
     // Main SwiftNIO package
     .package(url: "https://github.com/apple/swift-nio.git", from: "2.14.0"),
     // HTTP2 via SwiftNIO
-    .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.8.0"),
+    .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.12.1"),
     // TLS via SwiftNIO
     .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.6.2"),
     // Support for Network.framework where possible.

+ 9 - 2
Sources/GRPC/ClientConnection.swift

@@ -240,6 +240,9 @@ extension ClientConnection {
     /// The connection backoff configuration. If no connection retrying is required then this should
     /// be `nil`.
     public var connectionBackoff: ConnectionBackoff?
+    
+    /// The HTTP/2 flow control target window size.
+    public var httpTargetWindowSize: Int
 
     /// The HTTP protocol used for this connection.
     public var httpProtocol: HTTP2ToHTTP1ClientCodec.HTTPProtocol {
@@ -258,13 +261,15 @@ extension ClientConnection {
     /// - Parameter tlsConfiguration: TLS configuration, defaulting to `nil`.
     /// - Parameter connectionBackoff: The connection backoff configuration to use.
     /// - Parameter messageEncoding: Message compression configuration, defaults to no compression.
+    /// - Parameter targetWindowSize: The HTTP/2 flow control target window size.
     public init(
       target: ConnectionTarget,
       eventLoopGroup: EventLoopGroup,
       errorDelegate: ClientErrorDelegate? = LoggingClientErrorDelegate(),
       connectivityStateDelegate: ConnectivityStateDelegate? = nil,
       tls: Configuration.TLS? = nil,
-      connectionBackoff: ConnectionBackoff? = ConnectionBackoff()
+      connectionBackoff: ConnectionBackoff? = ConnectionBackoff(),
+      httpTargetWindowSize: Int = 65535
     ) {
       self.target = target
       self.eventLoopGroup = eventLoopGroup
@@ -272,6 +277,7 @@ extension ClientConnection {
       self.connectivityStateDelegate = connectivityStateDelegate
       self.tls = tls
       self.connectionBackoff = connectionBackoff
+      self.httpTargetWindowSize = httpTargetWindowSize
     }
   }
 }
@@ -324,6 +330,7 @@ extension Channel {
   }
 
   func configureGRPCClient(
+    httpTargetWindowSize: Int,
     tlsConfiguration: TLSConfiguration?,
     tlsServerHostname: String?,
     connectionManager: ConnectionManager,
@@ -335,7 +342,7 @@ extension Channel {
     }
 
     return (tlsConfigured ?? self.eventLoop.makeSucceededFuture(())).flatMap {
-      self.configureHTTP2Pipeline(mode: .client)
+      self.configureHTTP2Pipeline(mode: .client, targetWindowSize: httpTargetWindowSize)
     }.flatMap { _ in
       return self.pipeline.handler(type: NIOHTTP2Handler.self).flatMap { http2Handler in
         self.pipeline.addHandler(

+ 1 - 0
Sources/GRPC/ConnectionManager.swift

@@ -555,6 +555,7 @@ extension ConnectionManager {
       .channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
       .channelInitializer { channel in
         channel.configureGRPCClient(
+          httpTargetWindowSize: configuration.httpTargetWindowSize,
           tlsConfiguration: configuration.tls?.configuration,
           tlsServerHostname: serverHostname,
           connectionManager: self,

+ 12 - 1
Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift

@@ -36,6 +36,7 @@ extension ClientConnection {
     private var connectionBackoffIsEnabled = true
     private var errorDelegate: ClientErrorDelegate?
     private var connectivityStateDelegate: ConnectivityStateDelegate?
+    private var httpTargetWindowSize: Int = 65535
 
     fileprivate init(group: EventLoopGroup) {
       self.group = group
@@ -48,7 +49,8 @@ extension ClientConnection {
         errorDelegate: self.errorDelegate,
         connectivityStateDelegate: self.connectivityStateDelegate,
         tls: self.maybeTLS,
-        connectionBackoff: self.connectionBackoffIsEnabled ? self.connectionBackoff : nil
+        connectionBackoff: self.connectionBackoffIsEnabled ? self.connectionBackoff : nil,
+        httpTargetWindowSize: self.httpTargetWindowSize
       )
       return ClientConnection(configuration: configuration)
     }
@@ -195,6 +197,15 @@ extension ClientConnection.Builder.Secure {
   }
 }
 
+extension ClientConnection.Builder {
+  /// Sets the HTTP/2 flow control target window size. Defaults to 65,535 if not explicitly set.
+  @discardableResult
+  public func withHTTPTargetWindowSize(_ httpTargetWindowSize: Int) -> Self {
+    self.httpTargetWindowSize = httpTargetWindowSize
+    return self
+  }
+}
+
 fileprivate extension Double {
   static func seconds(from amount: TimeAmount) -> Double {
     return Double(amount.nanoseconds) / 1_000_000_000

+ 11 - 2
Sources/GRPC/HTTPProtocolSwitcher.swift

@@ -25,6 +25,7 @@ internal class HTTPProtocolSwitcher {
   private let handlersInitializer: ((Channel) -> EventLoopFuture<Void>)
   private let errorDelegate: ServerErrorDelegate?
   private let logger = Logger(subsystem: .serverChannelCall)
+  private let httpTargetWindowSize: Int
 
   // We could receive additional data after the initial data and before configuring
   // the pipeline; buffer it and fire it down the pipeline once it is configured.
@@ -41,8 +42,13 @@ internal class HTTPProtocolSwitcher {
   }
   private var bufferedData: [NIOAny] = []
 
-  init(errorDelegate: ServerErrorDelegate?, handlersInitializer: (@escaping (Channel) -> EventLoopFuture<Void>)) {
+  init(
+    errorDelegate: ServerErrorDelegate?,
+    httpTargetWindowSize: Int = 65535,
+    handlersInitializer: (@escaping (Channel) -> EventLoopFuture<Void>)
+  ) {
     self.errorDelegate = errorDelegate
+    self.httpTargetWindowSize = httpTargetWindowSize
     self.handlersInitializer = handlersInitializer
   }
 }
@@ -132,7 +138,10 @@ extension HTTPProtocolSwitcher: ChannelInboundHandler, RemovableChannelHandler {
           .cascade(to: pipelineConfigured)
 
       case .http2:
-        context.channel.configureHTTP2Pipeline(mode: .server) { (streamChannel, streamID) in
+        context.channel.configureHTTP2Pipeline(
+          mode: .server,
+          targetWindowSize: httpTargetWindowSize
+        ) { (streamChannel, streamID) in
             streamChannel.pipeline.addHandler(HTTP2ToHTTP1ServerCodec(streamID: streamID, normalizeHTTPHeaders: true))
               .flatMap { self.handlersInitializer(streamChannel) }
           }

+ 10 - 3
Sources/GRPC/Server.swift

@@ -99,7 +99,10 @@ public final class Server {
       .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
       // Set the handlers that are applied to the accepted Channels
       .childChannelInitializer { channel in
-        let protocolSwitcher = HTTPProtocolSwitcher(errorDelegate: configuration.errorDelegate) { channel -> EventLoopFuture<Void> in
+        let protocolSwitcher = HTTPProtocolSwitcher(
+          errorDelegate: configuration.errorDelegate,
+          httpTargetWindowSize: configuration.httpTargetWindowSize
+        ) { channel -> EventLoopFuture<Void> in
           let logger = Logger(subsystem: .serverChannelCall, metadata: [MetadataKey.requestID: "\(UUID())"])
           let handler = GRPCServerRequestRoutingHandler(
             servicesByName: configuration.serviceProvidersByName,
@@ -173,7 +176,6 @@ extension Server {
   public struct Configuration {
     /// The target to bind to.
     public var target: BindTarget
-
     /// The event loop group to run the connection on.
     public var eventLoopGroup: EventLoopGroup
 
@@ -196,6 +198,9 @@ extension Server {
     /// streaming and bidirectional streaming RPCs) by passing setting `compression` to `.disabled`
     /// in `sendResponse(_:compression)`.
     public var messageEncoding: ServerMessageEncoding
+    
+    /// The HTTP/2 flow control target window size.
+    public var httpTargetWindowSize: Int
 
     /// Create a `Configuration` with some pre-defined defaults.
     ///
@@ -212,7 +217,8 @@ extension Server {
       serviceProviders: [CallHandlerProvider],
       errorDelegate: ServerErrorDelegate? = LoggingServerErrorDelegate.shared,
       tls: TLS? = nil,
-      messageEncoding: ServerMessageEncoding = .disabled
+      messageEncoding: ServerMessageEncoding = .disabled,
+      httpTargetWindowSize: Int = 65535
     ) {
       self.target = target
       self.eventLoopGroup = eventLoopGroup
@@ -220,6 +226,7 @@ extension Server {
       self.errorDelegate = errorDelegate
       self.tls = tls
       self.messageEncoding = messageEncoding
+      self.httpTargetWindowSize = httpTargetWindowSize
     }
   }
 }

+ 12 - 1
Sources/GRPC/ServerBuilder.swift

@@ -23,6 +23,7 @@ extension Server {
     private var providers: [CallHandlerProvider] = []
     private var errorDelegate: ServerErrorDelegate?
     private var messageEncoding: ServerMessageEncoding = .disabled
+    private var httpTargetWindowSize: Int = 65535
 
     fileprivate init(group: EventLoopGroup) {
       self.group = group
@@ -50,7 +51,8 @@ extension Server {
         serviceProviders: self.providers,
         errorDelegate: self.errorDelegate,
         tls: self.maybeTLS,
-        messageEncoding: self.messageEncoding
+        messageEncoding: self.messageEncoding,
+        httpTargetWindowSize: self.httpTargetWindowSize
       )
       return Server.start(configuration: configuration)
     }
@@ -102,6 +104,15 @@ extension Server.Builder.Secure {
   }
 }
 
+extension Server.Builder {
+  /// Sets the HTTP/2 flow control target window size. Defaults to 65,535 if not explicitly set.
+  @discardableResult
+  public func withHTTPTargetWindowSize(_ httpTargetWindowSize: Int) -> Self {
+    self.httpTargetWindowSize = httpTargetWindowSize
+    return self
+  }
+}
+
 extension Server {
   /// Returns an insecure `Server` builder which is *not configured with TLS*.
   public static func insecure(group: EventLoopGroup) -> Builder {