Browse Source

Expose more http/2 configuration knobs (#1255)

Motivation:

Having more control over the configuration of the http/2 layer is useful
for certain performance sensitive situations. We recently exposed the
max frame size setting on the server (#1253); we should do the same on
the client.

Modifications:

- Add configuration for the max frame size to the client
- Fix the clamping of the max frame size on the server (this was missed
  when reviewing #1253)
- Add tests to validate that the configured values are correctly clamped
- Send `SETTINGS_INITIAL_WINDOW_SIZE` in the initial SETTINGS frame set to the
  value of the target window size.

Result:

Users have more control over HTTP/2 settings.
George Barnett 4 years ago
parent
commit
4a7231f6aa

+ 28 - 3
Sources/GRPC/ClientConnection.swift

@@ -16,6 +16,7 @@
 import Foundation
 import Logging
 import NIOCore
+import NIOHPACK
 import NIOHTTP2
 import NIOSSL
 import NIOTLS
@@ -340,8 +341,21 @@ extension ClientConnection {
     /// Defaults to `waitsForConnectivity`.
     public var callStartBehavior: CallStartBehavior = .waitsForConnectivity
 
-    /// The HTTP/2 flow control target window size. Defaults to 65535.
-    public var httpTargetWindowSize = 65535
+    /// The HTTP/2 flow control target window size. Defaults to 65535. Values are clamped between
+    /// 1 and 2^31-1 inclusive.
+    public var httpTargetWindowSize = 65535 {
+      didSet {
+        self.httpTargetWindowSize = self.httpTargetWindowSize.clamped(to: 1 ... Int(Int32.max))
+      }
+    }
+
+    /// The HTTP/2 max frame size. Defaults to 16384. Value is clamped between 2^14 and 2^24-1
+    /// octets inclusive (the minimum and maximum allowable values - HTTP/2 RFC 7540 4.2).
+    public var httpMaxFrameSize: Int = 16384 {
+      didSet {
+        self.httpMaxFrameSize = self.httpMaxFrameSize.clamped(to: 16384 ... 16_777_215)
+      }
+    }
 
     /// The HTTP protocol used for this connection.
     public var httpProtocol: HTTP2FramePayloadToHTTP1ClientCodec.HTTPProtocol {
@@ -500,12 +514,23 @@ extension ChannelPipeline.SynchronousOperations {
     connectionKeepalive: ClientConnectionKeepalive,
     connectionIdleTimeout: TimeAmount,
     httpTargetWindowSize: Int,
+    httpMaxFrameSize: Int,
     errorDelegate: ClientErrorDelegate?,
     logger: Logger
   ) throws {
+    let initialSettings = [
+      // As per the default settings for swift-nio-http2:
+      HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize),
+      // We never expect (or allow) server initiated streams.
+      HTTP2Setting(parameter: .maxConcurrentStreams, value: 0),
+      // As configured by the user.
+      HTTP2Setting(parameter: .maxFrameSize, value: httpMaxFrameSize),
+      HTTP2Setting(parameter: .initialWindowSize, value: httpTargetWindowSize),
+    ]
+
     // We could use 'configureHTTP2Pipeline' here, but we need to add a few handlers between the
     // two HTTP/2 handlers so we'll do it manually instead.
-    try self.addHandler(NIOHTTP2Handler(mode: .client))
+    try self.addHandler(NIOHTTP2Handler(mode: .client, initialSettings: initialSettings))
 
     let h2Multiplexer = HTTP2StreamMultiplexer(
       mode: .client,

+ 5 - 0
Sources/GRPC/ConnectionManagerChannelProvider.swift

@@ -50,6 +50,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
   internal var tlsConfiguration: GRPCTLSConfiguration?
 
   internal var httpTargetWindowSize: Int
+  internal var httpMaxFrameSize: Int
 
   internal var errorDelegate: Optional<ClientErrorDelegate>
   internal var debugChannelInitializer: Optional<(Channel) -> EventLoopFuture<Void>>
@@ -61,6 +62,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
     tlsMode: TLSMode,
     tlsConfiguration: GRPCTLSConfiguration?,
     httpTargetWindowSize: Int,
+    httpMaxFrameSize: Int,
     errorDelegate: ClientErrorDelegate?,
     debugChannelInitializer: ((Channel) -> EventLoopFuture<Void>)?
   ) {
@@ -72,6 +74,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
     self.tlsConfiguration = tlsConfiguration
 
     self.httpTargetWindowSize = httpTargetWindowSize
+    self.httpMaxFrameSize = httpMaxFrameSize
 
     self.errorDelegate = errorDelegate
     self.debugChannelInitializer = debugChannelInitializer
@@ -102,6 +105,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
       tlsMode: tlsMode,
       tlsConfiguration: configuration.tlsConfiguration,
       httpTargetWindowSize: configuration.httpTargetWindowSize,
+      httpMaxFrameSize: configuration.httpMaxFrameSize,
       errorDelegate: configuration.errorDelegate,
       debugChannelInitializer: configuration.debugChannelInitializer
     )
@@ -170,6 +174,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
             connectionKeepalive: self.connectionKeepalive,
             connectionIdleTimeout: self.connectionIdleTimeout,
             httpTargetWindowSize: self.httpTargetWindowSize,
+            httpMaxFrameSize: self.httpMaxFrameSize,
             errorDelegate: self.errorDelegate,
             logger: logger
           )

+ 13 - 0
Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift

@@ -388,11 +388,24 @@ extension ClientConnection.Builder.Secure {
 
 extension ClientConnection.Builder {
   /// Sets the HTTP/2 flow control target window size. Defaults to 65,535 if not explicitly set.
+  /// Values are clamped between 1 and 2^31-1 inclusive.
   @discardableResult
   public func withHTTPTargetWindowSize(_ httpTargetWindowSize: Int) -> Self {
     self.configuration.httpTargetWindowSize = httpTargetWindowSize
     return self
   }
+
+  /// Sets the maximum size of an HTTP/2 frame in bytes which the client is willing to receive from
+  /// the server. Defaults to 16384. Value are clamped between 2^14 and 2^24-1 octets inclusive
+  /// (the minimum and maximum permitted values per RFC 7540 § 4.2).
+  ///
+  /// Raising this value may lower CPU usage for large message at the cost of increasing head of
+  /// line blocking for small messages.
+  @discardableResult
+  public func withHTTPMaxFrameSize(_ httpMaxFrameSize: Int) -> Self {
+    self.configuration.httpMaxFrameSize = httpMaxFrameSize
+    return self
+  }
 }
 
 extension ClientConnection.Builder {

+ 4 - 0
Sources/GRPC/GRPCServerPipelineConfigurator.swift

@@ -94,6 +94,10 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan
           parameter: .maxFrameSize,
           value: self.configuration.httpMaxFrameSize
         ),
+        HTTP2Setting(
+          parameter: .initialWindowSize,
+          value: self.configuration.httpTargetWindowSize
+        ),
       ]
     )
   }

+ 10 - 5
Sources/GRPC/Server.swift

@@ -322,8 +322,13 @@ extension Server {
       }
     }
 
-    /// The HTTP/2 flow control target window size. Defaults to 65535.
-    public var httpTargetWindowSize: Int = 65535
+    /// The HTTP/2 flow control target window size. Defaults to 65535. Values are clamped between
+    /// 1 and 2^31-1 inclusive.
+    public var httpTargetWindowSize = 65535 {
+      didSet {
+        self.httpTargetWindowSize = self.httpTargetWindowSize.clamped(to: 1 ... Int(Int32.max))
+      }
+    }
 
     /// The HTTP/2 max number of concurrent streams. Defaults to 100. Must be non-negative.
     public var httpMaxConcurrentStreams: Int = 100 {
@@ -335,8 +340,8 @@ extension Server {
     /// The HTTP/2 max frame size. Defaults to 16384. Value is clamped between 2^14 and 2^24-1
     /// octets inclusive (the minimum and maximum allowable values - HTTP/2 RFC 7540 4.2).
     public var httpMaxFrameSize: Int = 16384 {
-      didSet(httpMaxFrameSize) {
-        self.httpMaxFrameSize = httpMaxFrameSize.clamped(to: 16384 ... 16_777_215)
+      didSet {
+        self.httpMaxFrameSize = self.httpMaxFrameSize.clamped(to: 16384 ... 16_777_215)
       }
     }
 
@@ -457,7 +462,7 @@ private extension ServerBootstrapProtocol {
 }
 
 extension Comparable {
-  fileprivate func clamped(to range: ClosedRange<Self>) -> Self {
+  internal func clamped(to range: ClosedRange<Self>) -> Self {
     return min(max(self, range.lowerBound), range.upperBound)
   }
 }

+ 8 - 4
Sources/GRPC/ServerBuilder.swift

@@ -153,22 +153,26 @@ extension Server.Builder.Secure {
 
 extension Server.Builder {
   /// Sets the HTTP/2 flow control target window size. Defaults to 65,535 if not explicitly set.
+  /// Values are clamped between 1 and 2^31-1 inclusive.
   @discardableResult
   public func withHTTPTargetWindowSize(_ httpTargetWindowSize: Int) -> Self {
     self.configuration.httpTargetWindowSize = httpTargetWindowSize
     return self
   }
-}
 
-extension Server.Builder {
+  /// Sets the maximum allowed number of concurrent HTTP/2 streams a client may open for a given
+  /// connection. Defaults to 100.
   @discardableResult
   public func withHTTPMaxConcurrentStreams(_ httpMaxConcurrentStreams: Int) -> Self {
     self.configuration.httpMaxConcurrentStreams = httpMaxConcurrentStreams
     return self
   }
-}
 
-extension Server.Builder {
+  /// Sets the HTTP/2 max frame size. Defaults to 16384. Value are clamped between 2^14 and 2^24-1
+  /// octets inclusive (the minimum and maximum permitted values per RFC 7540 § 4.2).
+  ///
+  /// Raising this value may lower CPU usage for large message at the cost of increasing head of
+  /// line blocking for small messages.
   @discardableResult
   public func withHTTPMaxFrameSize(_ httpMaxFrameSize: Int) -> Self {
     self.configuration.httpMaxFrameSize = httpMaxFrameSize

+ 98 - 0
Tests/GRPCTests/ConfigurationTests.swift

@@ -0,0 +1,98 @@
+/*
+ * Copyright 2021, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import GRPC
+import NIOEmbedded
+import XCTest
+
+final class ConfigurationTests: GRPCTestCase {
+  private var eventLoop: EmbeddedEventLoop!
+
+  private var clientDefaults: ClientConnection.Configuration {
+    return .default(target: .unixDomainSocket("/ignored"), eventLoopGroup: self.eventLoop)
+  }
+
+  private var serverDefaults: Server.Configuration {
+    return .default(
+      target: .unixDomainSocket("/ignored"),
+      eventLoopGroup: self.eventLoop,
+      serviceProviders: []
+    )
+  }
+
+  override func setUp() {
+    super.setUp()
+    self.eventLoop = EmbeddedEventLoop()
+  }
+
+  override func tearDown() {
+    XCTAssertNoThrow(try self.eventLoop.syncShutdownGracefully())
+    super.tearDown()
+  }
+
+  private let maxFrameSizeMinimum = (1 << 14)
+  private let maxFrameSizeMaximum = (1 << 24) - 1
+
+  private func doTestHTTPMaxFrameSizeIsClamped(for configuration: HasHTTP2Configuration) {
+    var configuration = configuration
+    configuration.httpMaxFrameSize = 0
+    XCTAssertEqual(configuration.httpMaxFrameSize, self.maxFrameSizeMinimum)
+
+    configuration.httpMaxFrameSize = .max
+    XCTAssertEqual(configuration.httpMaxFrameSize, self.maxFrameSizeMaximum)
+
+    configuration.httpMaxFrameSize = self.maxFrameSizeMinimum + 1
+    XCTAssertEqual(configuration.httpMaxFrameSize, self.maxFrameSizeMinimum + 1)
+  }
+
+  func testHTTPMaxFrameSizeIsClampedForClient() {
+    self.doTestHTTPMaxFrameSizeIsClamped(for: self.clientDefaults)
+  }
+
+  func testHTTPMaxFrameSizeIsClampedForServer() {
+    self.doTestHTTPMaxFrameSizeIsClamped(for: self.serverDefaults)
+  }
+
+  private let targetWindowSizeMinimum = 1
+  private let targetWindowSizeMaximum = Int(Int32.max)
+
+  private func doTestHTTPTargetWindowSizeIsClamped(for configuration: HasHTTP2Configuration) {
+    var configuration = configuration
+    configuration.httpTargetWindowSize = .min
+    XCTAssertEqual(configuration.httpTargetWindowSize, self.targetWindowSizeMinimum)
+
+    configuration.httpTargetWindowSize = .max
+    XCTAssertEqual(configuration.httpTargetWindowSize, self.targetWindowSizeMaximum)
+
+    configuration.httpTargetWindowSize = self.targetWindowSizeMinimum + 1
+    XCTAssertEqual(configuration.httpTargetWindowSize, self.targetWindowSizeMinimum + 1)
+  }
+
+  func testHTTPTargetWindowSizeIsClampedForClient() {
+    self.doTestHTTPTargetWindowSizeIsClamped(for: self.clientDefaults)
+  }
+
+  func testHTTPTargetWindowSizeIsClampedForServer() {
+    self.doTestHTTPTargetWindowSizeIsClamped(for: self.serverDefaults)
+  }
+}
+
+private protocol HasHTTP2Configuration {
+  var httpMaxFrameSize: Int { get set }
+  var httpTargetWindowSize: Int { get set }
+}
+
+extension ClientConnection.Configuration: HasHTTP2Configuration {}
+extension Server.Configuration: HasHTTP2Configuration {}