From 0485955f5aa51808e3b04d03b20266d4a3397569 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 23 Sep 2026 17:52:05 +0100 Subject: [PATCH] Move graceful shutdown configuration --- .../NIOHTTPServerConfiguration.swift | 68 ++++++++++--------- .../HTTP2+SwiftConfiguration.swift | 21 +----- .../NIOHTTPServer+SwiftConfiguration.swift | 25 +++++++ .../SwiftConfigurationIntegration.md | 10 +-- .../NIOHTTPServer+SecureUpgrade.swift | 2 +- .../NIOHTTPServer+ServiceLifecycleTests.swift | 18 ++--- ...NIOHTTPServerSwiftConfigurationTests.swift | 42 +++++++++--- 7 files changed, 106 insertions(+), 80 deletions(-) diff --git a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift index 56ec274..da1200b 100644 --- a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift @@ -137,42 +137,19 @@ public struct NIOHTTPServerConfiguration: Sendable { /// The number of concurrent streams on the HTTP/2 connection. public var maxConcurrentStreams: Int - /// The graceful shutdown configuration. - public var gracefulShutdown: GracefulShutdownConfiguration - - /// Configuration options for HTTP/2 graceful shutdown behavior. - public struct GracefulShutdownConfiguration: Sendable, Hashable { - /// The maximum amount of time that the connection has to close gracefully. - /// If set to `nil`, no time limit is enforced on the graceful shutdown process. - public var maximumGracefulShutdownDuration: Duration? - - /// Creates a graceful shutdown configuration with the specified timeout value. - /// - /// - Parameters: - /// - maximumGracefulShutdownDuration: The maximum amount of time that the connection has to close - /// gracefully. When `nil`, no time limit is enforced for active streams to finish during graceful - /// shutdown. - public init(maximumGracefulShutdownDuration: Duration? = nil) { - self.maximumGracefulShutdownDuration = maximumGracefulShutdownDuration - } - } - /// - Parameters: /// - maxFrameSize: The maximum frame size to be used in connections. /// - targetWindowSize: The target window size for connections. This will also be set as the initial window /// size. /// - maxConcurrentStreams: The maximum number of concurrent streams permitted on connections. - /// - gracefulShutdown: The graceful shutdown configuration. public init( maxFrameSize: Int = Self.defaultMaxFrameSize, targetWindowSize: Int = Self.defaultTargetWindowSize, - maxConcurrentStreams: Int = Self.defaultMaxConcurrentStreams, - gracefulShutdown: GracefulShutdownConfiguration = .init() + maxConcurrentStreams: Int = Self.defaultMaxConcurrentStreams ) { self.maxFrameSize = maxFrameSize self.targetWindowSize = targetWindowSize self.maxConcurrentStreams = maxConcurrentStreams - self.gracefulShutdown = gracefulShutdown } @inlinable @@ -190,8 +167,7 @@ public struct NIOHTTPServerConfiguration: Sendable { Self( maxFrameSize: Self.defaultMaxFrameSize, targetWindowSize: Self.defaultTargetWindowSize, - maxConcurrentStreams: Self.defaultMaxConcurrentStreams, - gracefulShutdown: GracefulShutdownConfiguration() + maxConcurrentStreams: Self.defaultMaxConcurrentStreams ) } } @@ -288,6 +264,30 @@ public struct NIOHTTPServerConfiguration: Sendable { public static var defaults: Self { .init() } } + /// Configuration options for graceful shutdown behavior. + public struct GracefulShutdownConfiguration: Sendable, Hashable { + /// The maximum amount of time that the connection has to close gracefully before being forcefully closed. + /// If set to `nil`, no time limit is enforced for the connection to close gracefully. + public var maximumGracefulShutdownDuration: Duration? + + /// Creates a graceful shutdown configuration with the specified timeout value. + /// + /// - Parameters: + /// - maximumGracefulShutdownDuration: The maximum amount of time that the connection has to close gracefully. + /// If set to `nil`, no time limit is enforced for the connection to close gracefully. + public init(maximumGracefulShutdownDuration: Duration? = nil) { + self.maximumGracefulShutdownDuration = maximumGracefulShutdownDuration + } + + /// The default graceful shutdown configuration. + /// + /// Uses the following default values: + /// - ``maximumGracefulShutdownDuration``: `nil` (no time limit enforced for the connection to close gracefully). + public static var defaults: Self { + Self(maximumGracefulShutdownDuration: nil) + } + } + /// Network binding configuration specifying all addresses where the server should listen. /// /// - Precondition: Must not be empty. @@ -361,6 +361,9 @@ public struct NIOHTTPServerConfiguration: Sendable { /// Configuration for connection timeouts. public var connectionTimeouts: ConnectionTimeouts + /// The graceful shutdown configuration. + public var gracefulShutdown: GracefulShutdownConfiguration + /// The `NIOSSLContext` used by the secure upgrade channel(s), derived when the configuration is validated. /// /// `nil` when the configuration doesn't call for a secure upgrade channel, i.e. plaintext HTTP/1.1 or HTTP/3 only. @@ -381,9 +384,9 @@ public struct NIOHTTPServerConfiguration: Sendable { /// Create a new configuration with multiple bind targets. /// - /// Other configuration properties (``backpressureStrategy``, ``maxConnections``, - /// ``connectionTimeouts``) are initialized to their defaults and can be set on the resulting - /// value before passing it to ``NIOHTTPServer``. + /// Other configuration properties (``backpressureStrategy``, ``maxConnections``, ``connectionTimeouts``, + /// ``gracefulShutdown``) are initialized to their defaults and can be set on the resulting value before passing it + /// to ``NIOHTTPServer``. /// /// - Parameters: /// - bindTargets: An array of ``BindTarget`` values specifying where the server should listen. @@ -408,6 +411,7 @@ public struct NIOHTTPServerConfiguration: Sendable { self.backpressureStrategy = .defaults self.maxConnections = nil self.connectionTimeouts = .defaults + self.gracefulShutdown = .defaults // Validate the compatibility of `supportedHTTPVersions` and `transportSecurity`. try self.validateTransportConfiguration() @@ -415,9 +419,9 @@ public struct NIOHTTPServerConfiguration: Sendable { /// Create a new configuration with a single bind target. /// - /// Other configuration properties (``backpressureStrategy``, ``maxConnections``, - /// ``connectionTimeouts``) are initialized to their defaults and can be set on the resulting - /// value before passing it to ``NIOHTTPServer``. + /// Other configuration properties (``backpressureStrategy``, ``maxConnections``, ``connectionTimeouts``, + /// ``gracefulShutdown``) are initialized to their defaults and can be set on the resulting value before passing it + /// to ``NIOHTTPServer``. /// /// - Parameters: /// - bindTarget: A ``BindTarget``. diff --git a/Sources/NIOHTTPServer/Configuration/SwiftConfiguration/HTTP2+SwiftConfiguration.swift b/Sources/NIOHTTPServer/Configuration/SwiftConfiguration/HTTP2+SwiftConfiguration.swift index d9e651f..2d630fd 100644 --- a/Sources/NIOHTTPServer/Configuration/SwiftConfiguration/HTTP2+SwiftConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/SwiftConfiguration/HTTP2+SwiftConfiguration.swift @@ -25,8 +25,6 @@ extension NIOHTTPServerConfiguration.HTTP2 { /// connection. /// - `maxConcurrentStreams` (int, optional, default: 100): The maximum number of concurrent streams in an HTTP/2 /// connection. - /// - `gracefulShutdown.maximumDuration` (int, optional, default: nil): The maximum amount of time (in seconds) that - /// the connection has to close gracefully. /// /// - Parameter config: The configuration reader. public init(config: ConfigSnapshotReader) { @@ -39,24 +37,7 @@ extension NIOHTTPServerConfiguration.HTTP2 { forKey: "targetWindowSize", default: NIOHTTPServerConfiguration.HTTP2.defaultTargetWindowSize ), - maxConcurrentStreams: config.int(forKey: "maxConcurrentStreams", default: 100), - gracefulShutdown: .init(config: config.scoped(to: "gracefulShutdown")) - ) - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServerConfiguration.HTTP2.GracefulShutdownConfiguration { - /// Initialize a HTTP/2 graceful shutdown configuration from a config reader. - /// - /// ## Configuration keys: - /// - `maximumDuration` (int, optional, default: nil): The maximum amount of time (in seconds) that the connection - /// has to close gracefully. - /// - /// - Parameter config: The configuration reader. - public init(config: ConfigSnapshotReader) { - self.init( - maximumGracefulShutdownDuration: config.int(forKey: "maximumDuration").map { .seconds($0) } + maxConcurrentStreams: config.int(forKey: "maxConcurrentStreams", default: 100) ) } } diff --git a/Sources/NIOHTTPServer/Configuration/SwiftConfiguration/NIOHTTPServer+SwiftConfiguration.swift b/Sources/NIOHTTPServer/Configuration/SwiftConfiguration/NIOHTTPServer+SwiftConfiguration.swift index 620d3d9..e2c333c 100644 --- a/Sources/NIOHTTPServer/Configuration/SwiftConfiguration/NIOHTTPServer+SwiftConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/SwiftConfiguration/NIOHTTPServer+SwiftConfiguration.swift @@ -47,6 +47,15 @@ extension NIOHTTPServerConfiguration { /// /// - **`"backpressureStrategy"`**: The backpressure strategy (see ``BackPressureStrategy/init(config:)``). /// + /// - **`"maxConnections"`** (int, optional, default: nil): The maximum number of concurrent connections the server + /// will accept. When omitted, no limit is imposed. + /// + /// - **`"connectionTimeouts"`**: The idle, header read, and body read timeouts applied to connections (see + /// ``ConnectionTimeouts/init(config:)``). + /// + /// - **`"gracefulShutdown"`**: The graceful shutdown behavior of connections (see + /// ``GracefulShutdownConfiguration/init(config:)``). + /// /// - Parameters: /// - config: The configuration reader to read configuration values from. /// - customCertificateVerificationCallback: A custom client certificate verification callback. This must be @@ -80,6 +89,7 @@ extension NIOHTTPServerConfiguration { self.backpressureStrategy = .init(config: snapshot.scoped(to: "backpressureStrategy")) self.maxConnections = snapshot.int(forKey: "maxConnections") self.connectionTimeouts = .init(config: snapshot.scoped(to: "connectionTimeouts")) + self.gracefulShutdown = .init(config: snapshot.scoped(to: "gracefulShutdown")) } /// Reads bind targets from either the singular `bindTarget` scope or the plural `bindTargets` scope. @@ -493,4 +503,19 @@ extension NIOHTTPServerConfiguration.ConnectionTimeouts { } } +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration.GracefulShutdownConfiguration { + /// Initialize a graceful shutdown configuration from a config reader. + /// + /// ## Configuration keys: + /// - `maximumDuration` (int, optional, default: nil): The maximum amount of time (in seconds) that the connection + /// has to close gracefully. + /// + /// - Parameter config: The configuration reader. + public init(config: ConfigSnapshotReader) { + self.init( + maximumGracefulShutdownDuration: config.int(forKey: "maximumDuration").map { .seconds($0) } + ) + } +} #endif // Configuration diff --git a/Sources/NIOHTTPServer/Documentation.docc/SwiftConfigurationIntegration.md b/Sources/NIOHTTPServer/Documentation.docc/SwiftConfigurationIntegration.md index 1b81439..384dcc1 100644 --- a/Sources/NIOHTTPServer/Documentation.docc/SwiftConfigurationIntegration.md +++ b/Sources/NIOHTTPServer/Documentation.docc/SwiftConfigurationIntegration.md @@ -50,7 +50,6 @@ its respective key prefix. | `http.http2` | `maxFrameSize` | `int` (bytes) | Optional | 2^14 | | | `targetWindowSize` | `int` (bytes) | Optional | 2^16-1 | | | `maxConcurrentStreams` | `int` | Optional | 100 | -| `http.http2.gracefulShutdown` | `maximumDuration` | `int` (seconds) | Optional | nil | | `http.http3` | `preferHuffmanEncoding` | `bool` | Optional | true | | `http.http3.connectionSettings` | `qpackMaximumTableCapacity` | `int` | Optional | 0 | | | `qpackBlockedStreams` | `int` | Optional | 0 | @@ -92,6 +91,7 @@ its respective key prefix. | `connectionTimeouts` | `idle` | `int` | Optional | nil | | | `readHeader` | `int` | Optional | nil | | | `readBody` | `int` | Optional | nil | +| `gracefulShutdown` | `maximumDuration` | `int` (seconds) | Optional | nil | #### (m)TLS credentials @@ -139,10 +139,7 @@ key were omitted. "http2": { "maxFrameSize": 16384, // default: 2^14 (16384) "targetWindowSize": 65535, // default: 2^16 - 1 (65535) - "maxConcurrentStreams": 100, // default: 100 - "gracefulShutdown": { - "maximumDuration": 30 // default: nil (no time limit) - } + "maxConcurrentStreams": 100 // default: 100 } }, "transportSecurity": { @@ -163,6 +160,9 @@ key were omitted. "idle": 60, // default: nil (no timeout) "readHeader": 30, // default: nil (no timeout) "readBody": 60 // default: nil (no timeout) + }, + "gracefulShutdown": { + "maximumDuration": 30 // default: nil (no time limit) } } ``` diff --git a/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift b/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift index ad8c254..5866c04 100644 --- a/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift +++ b/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift @@ -271,7 +271,7 @@ extension NIOHTTPServer { connectionManagerConfiguration: .init( maxIdleTime: self.configuration.connectionTimeouts.idle.map { TimeAmount($0) }, maxAge: nil, - maxGraceTime: configuration.gracefulShutdown.maximumGracefulShutdownDuration + maxGraceTime: self.configuration.gracefulShutdown.maximumGracefulShutdownDuration .map { TimeAmount($0) }, keepalive: nil ), diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift index 34bd8f4..b97c220 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift @@ -284,19 +284,15 @@ struct NIOHTTPServiceLifecycleTests { func testActiveHTTP2ConnectionIsShutDownAfterGraceTimeout() async throws { let (leafPath, caPath, keyPath) = try TestCA.makeSelfSignedChain().writeToDisk() - let server = NIOHTTPServer( - logger: self.serverLogger, - configuration: try .init( - bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), - supportedHTTPVersions: [ - .http1_1, - .http2(config: .init(gracefulShutdown: .init(maximumGracefulShutdownDuration: .milliseconds(500)))), - ], - transportSecurity: .tls( - credentials: .x509(.pemFile(certificateChainPath: leafPath, privateKeyPath: keyPath)) - ) + var configuration = try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http1_1, .http2], + transportSecurity: .tls( + credentials: .x509(.pemFile(certificateChainPath: leafPath, privateKeyPath: keyPath)) ) ) + configuration.gracefulShutdown = .init(maximumGracefulShutdownDuration: .milliseconds(500)) + let server = NIOHTTPServer(logger: self.serverLogger, configuration: configuration) // This promise will be fulfilled when the server receives the first part of the request body. Once this // happens, we can initiate the graceful shutdown. diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerSwiftConfigurationTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerSwiftConfigurationTests.swift index cdb3f59..fd070cc 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerSwiftConfigurationTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerSwiftConfigurationTests.swift @@ -314,7 +314,6 @@ struct NIOHTTPServerSwiftConfigurationTests { #expect(http2.maxFrameSize == NIOHTTPServerConfiguration.HTTP2.defaultMaxFrameSize) #expect(http2.targetWindowSize == NIOHTTPServerConfiguration.HTTP2.defaultTargetWindowSize) #expect(http2.maxConcurrentStreams == 100) - #expect(http2.gracefulShutdown == .init(maximumGracefulShutdownDuration: nil)) } @Test("Custom values") @@ -324,7 +323,6 @@ struct NIOHTTPServerSwiftConfigurationTests { "maxFrameSize": 1, "targetWindowSize": 2, "maxConcurrentStreams": 3, - "gracefulShutdown.maximumDuration": 4, ]) let config = ConfigReader(provider: provider) let snapshot = config.snapshot() @@ -334,7 +332,6 @@ struct NIOHTTPServerSwiftConfigurationTests { #expect(http2.maxFrameSize == 1) #expect(http2.targetWindowSize == 2) #expect(http2.maxConcurrentStreams == 3) - #expect(http2.gracefulShutdown.maximumGracefulShutdownDuration == .seconds(4)) } @Test("Partial custom values") @@ -349,7 +346,6 @@ struct NIOHTTPServerSwiftConfigurationTests { #expect(http2.maxFrameSize == 5) #expect(http2.targetWindowSize == NIOHTTPServerConfiguration.HTTP2.defaultTargetWindowSize) #expect(http2.maxConcurrentStreams == 100) - #expect(http2.gracefulShutdown.maximumGracefulShutdownDuration == nil) } } @@ -1144,6 +1140,32 @@ struct NIOHTTPServerSwiftConfigurationTests { } } + @Suite("GracefulShutdownConfiguration") + struct GracefulShutdownConfigurationTests { + @available(anyAppleOS 26.0, *) + @Test("Default value") func defaultValue() { + let provider = InMemoryProvider(values: [:]) + let config = ConfigReader(provider: provider) + let snapshot = config.snapshot() + + let gracefulShutdown = NIOHTTPServerConfiguration.GracefulShutdownConfiguration(config: snapshot) + + #expect(gracefulShutdown == .defaults) + #expect(gracefulShutdown.maximumGracefulShutdownDuration == nil) + } + + @available(anyAppleOS 26.0, *) + @Test("Custom value") func customValue() { + let provider = InMemoryProvider(values: ["maximumDuration": 42]) + let config = ConfigReader(provider: provider) + let snapshot = config.snapshot() + + let gracefulShutdown = NIOHTTPServerConfiguration.GracefulShutdownConfiguration(config: snapshot) + + #expect(gracefulShutdown.maximumGracefulShutdownDuration == .seconds(42)) + } + } + @Suite("End-to-End") struct EndToEndConfigurationTests { @Test("Configure all possible values") @@ -1161,7 +1183,7 @@ struct NIOHTTPServerSwiftConfigurationTests { "http.http2.maxFrameSize": 1, "http.http2.targetWindowSize": 2, "http.http2.maxConcurrentStreams": 3, - "http.http2.gracefulShutdown.maximumDuration": 4, + "gracefulShutdown.maximumDuration": 4, "transportSecurity.mode": .init(.string("mTLS"), isSecret: false), "transportSecurity.credentialSource": .init(.string("inline"), isSecret: false), "transportSecurity.certificateChainPEMString": .init(.string(certsPEM), isSecret: false), @@ -1187,12 +1209,10 @@ struct NIOHTTPServerSwiftConfigurationTests { #expect(serverConfig.supportedHTTPVersions.contains(.http1_1)) #expect( serverConfig.supportedHTTPVersions.http2ConfigIfSupported - == .init( - maxFrameSize: 1, - targetWindowSize: 2, - maxConcurrentStreams: 3, - gracefulShutdown: .init(maximumGracefulShutdownDuration: .seconds(4)) - ) + == .init(maxFrameSize: 1, targetWindowSize: 2, maxConcurrentStreams: 3) + ) + #expect( + serverConfig.gracefulShutdown == .init(maximumGracefulShutdownDuration: .seconds(4)) ) guard case .mTLS(let tlsCredentials, let trustConfig) = serverConfig.transportSecurity.backing else {