Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/NIOHTTPServer/NIOHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public struct NIOHTTPServer: HTTPServer {
}

/// Creates and returns server channels based on the configured transport security.
private func makeServerChannels() async throws -> [ServerChannel] {
func makeServerChannels() async throws -> [ServerChannel] {
// If transport security is `plaintext`, we can only create an HTTP/1.1 channel.
if case .plaintext = self.configuration.transportSecurity.backing {
let http1Channels = try await self.setupHTTP1_1ServerChannels(bindTargets: self.configuration.bindTargets)
Expand Down Expand Up @@ -417,7 +417,7 @@ public struct NIOHTTPServer: HTTPServer {
}

/// Forcefully closes the server channels without waiting for existing connections to drain.
private func close(serverChannels: [ServerChannel]) {
func close(serverChannels: [ServerChannel]) {
self.finishListeningAddressPromise()

for serverChannel in serverChannels {
Expand Down
234 changes: 110 additions & 124 deletions Tests/NIOHTTPServerTests/ConnectionBackpressureEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,181 +23,167 @@ import Testing

@Suite("Connection Backpressure End-to-End")
struct ConnectionBackpressureEndToEndTests {
let serverLogger = Logger(label: "ConnectionBackpressureE2ETests")
let serverLogger = Logger(label: "ConnectionBackpressureE2ETests.server")
let clientLogger = Logger(label: "ConnectionBackpressureE2ETests.client")

@available(anyAppleOS 26.0, *)
@Test("Requests succeed under connection limit")
func requestsSucceedUnderConnectionLimit() async throws {
var configuration = try NIOHTTPServerConfiguration(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 0),
supportedHTTPVersions: [.http1_1],
transportSecurity: .plaintext
)
configuration.maxConnections = 2
configuration.connectionTimeouts = .init(idle: nil, readHeader: nil, readBody: nil)

let server = NIOHTTPServer(
logger: self.serverLogger,
configuration: configuration
)
@Test(
"Requests succeed under connection limit",
arguments: [NIOHTTPServer.HTTPVersion.plaintextHTTP1_1, .http1_1, .http2]
)
func requestsSucceedUnderConnectionLimit(httpVersion: NIOHTTPServer.HTTPVersion) async throws {
let (server, clientConfiguration) = try TestHelpers.makeServerAndClientConfiguration(
for: httpVersion,
clientLogger: self.clientLogger,
serverLogger: self.serverLogger
) { configuration in
configuration.maxConnections = 2
configuration.connectionTimeouts = .init(idle: nil, readHeader: nil, readBody: nil)
}

try await confirmation(expectedCount: 2) { responseReceived in
try await NIOHTTPServerTests.withServer(
try await TestHelpers.withServer(
server: server,
serverHandler: HTTPServerClosureRequestHandler { _, _, reader, responseSender in
try await NIOHTTPServerTests.echoResponse(
try await TestHelpers.echoResponse(
readUpTo: 1024,
reader: reader,
sender: responseSender
)
},
body: { serverAddress in
try await withThrowingTaskGroup { group in
for _ in 0..<2 {
group.addTask {
let client = try await ClientBootstrap(
group: .singletonMultiThreadedEventLoopGroup
).connectToTestHTTP1Server(at: serverAddress)

try await client.executeThenClose { inbound, outbound in
try await outbound.write(
.head(.init(method: .get, scheme: "http", authority: "", path: "/"))
)
try await outbound.write(.end(nil))

try await NIOHTTPServerTests.validateResponse(
inbound,
expectedHead: [NIOHTTPServerTests.responseHead(status: .ok, for: .http1_1)],
expectedBody: [],
expectStreamEnd: false
)

responseReceived()
}
}
) { serverAddress in
try await withThrowingTaskGroup { group in
for _ in 0..<2 {
group.addTask {
try await TestClientConnection.withConnectedRequestChannel(
configuration: clientConfiguration,
serverAddress: serverAddress
) { inbound, outbound in
try await outbound.write(.testHead(method: .get, for: httpVersion))
try await outbound.write(.end(nil))

try await TestHelpers.validateResponse(
inbound,
expectedHead: [.makeResponse(status: .ok, for: httpVersion)],
expectedBody: [],
expectStreamEnd: false
)

responseReceived()
}
}

try await group.waitForAll()
}

try await group.waitForAll()
}
)
}
}
}

@available(anyAppleOS 26.0, *)
@Test("More connections than maxConnections all eventually complete")
func moreConnectionsThanLimitAllComplete() async throws {
var configuration = try NIOHTTPServerConfiguration(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 0),
supportedHTTPVersions: [.http1_1],
transportSecurity: .plaintext
)
configuration.maxConnections = 2
configuration.connectionTimeouts = .init(idle: nil, readHeader: nil, readBody: nil)

let server = NIOHTTPServer(
logger: self.serverLogger,
configuration: configuration
)
@Test(
"More connections than maxConnections all eventually complete",
arguments: [NIOHTTPServer.HTTPVersion.plaintextHTTP1_1, .http1_1, .http2]
)
func moreConnectionsThanLimitAllComplete(httpVersion: NIOHTTPServer.HTTPVersion) async throws {
let (server, clientConfiguration) = try TestHelpers.makeServerAndClientConfiguration(
for: httpVersion,
clientLogger: self.clientLogger,
serverLogger: self.serverLogger
) { configuration in
configuration.maxConnections = 2
configuration.connectionTimeouts = .init(idle: nil, readHeader: nil, readBody: nil)
}

// Open 5 connections with maxConnections: 2. All should eventually complete
// as the connection limit handler releases slots when connections close.
let numConnections = 5
try await confirmation(expectedCount: numConnections) { responseReceived in
try await NIOHTTPServerTests.withServer(
try await TestHelpers.withServer(
server: server,
serverHandler: HTTPServerClosureRequestHandler { _, _, reader, responseSender in
try await NIOHTTPServerTests.echoResponse(
try await TestHelpers.echoResponse(
readUpTo: 1024,
reader: reader,
sender: responseSender
)
},
body: { serverAddress in
await withThrowingTaskGroup { group in
for _ in 0..<numConnections {
group.addTask {
let client = try await ClientBootstrap(
group: .singletonMultiThreadedEventLoopGroup
).connectToTestHTTP1Server(at: serverAddress)

try await client.executeThenClose { inbound, outbound in
try await outbound.write(
.head(.init(method: .get, scheme: "http", authority: "", path: "/"))
)
try await outbound.write(.end(nil))

try await NIOHTTPServerTests.validateResponse(
inbound,
expectedHead: [NIOHTTPServerTests.responseHead(status: .ok, for: .http1_1)],
expectedBody: [],
expectStreamEnd: false
)

responseReceived()
}
) { serverAddress in
await withThrowingTaskGroup { group in
for _ in 0..<numConnections {
group.addTask {
try await TestClientConnection.withConnectedRequestChannel(
configuration: clientConfiguration,
serverAddress: serverAddress
) { inbound, outbound in
try await outbound.write(.testHead(method: .get, for: httpVersion))
try await outbound.write(.end(nil))

try await TestHelpers.validateResponse(
inbound,
expectedHead: [.makeResponse(status: .ok, for: httpVersion)],
expectedBody: [],
expectStreamEnd: false
)

responseReceived()
}
}
}
}
)
}
}
}

@available(anyAppleOS 26.0, *)
@Test("No connection limit by default")
func noConnectionLimitByDefault() async throws {
var configuration = try NIOHTTPServerConfiguration(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 0),
supportedHTTPVersions: [.http1_1],
transportSecurity: .plaintext
)
configuration.connectionTimeouts = .init(idle: nil, readHeader: nil, readBody: nil)

let server = NIOHTTPServer(
logger: self.serverLogger,
configuration: configuration
)
@Test(
"No connection limit by default",
arguments: [NIOHTTPServer.HTTPVersion.plaintextHTTP1_1, .http1_1, .http2]
)
func noConnectionLimitByDefault(httpVersion: NIOHTTPServer.HTTPVersion) async throws {
let (server, clientConfiguration) = try TestHelpers.makeServerAndClientConfiguration(
for: httpVersion,
clientLogger: self.clientLogger,
serverLogger: self.serverLogger
) { configuration in
configuration.connectionTimeouts = .init(idle: nil, readHeader: nil, readBody: nil)
}

let numConnections = 5
try await confirmation(expectedCount: numConnections) { responseReceived in
try await NIOHTTPServerTests.withServer(
try await TestHelpers.withServer(
server: server,
serverHandler: HTTPServerClosureRequestHandler { _, _, reader, responseSender in
try await NIOHTTPServerTests.echoResponse(
try await TestHelpers.echoResponse(
readUpTo: 1024,
reader: reader,
sender: responseSender
)
},
body: { serverAddress in
await withThrowingTaskGroup { group in
for _ in 0..<numConnections {
group.addTask {
let client = try await ClientBootstrap(
group: .singletonMultiThreadedEventLoopGroup
).connectToTestHTTP1Server(at: serverAddress)

try await client.executeThenClose { inbound, outbound in
try await outbound.write(
.head(.init(method: .get, scheme: "http", authority: "", path: "/"))
)
try await outbound.write(.end(nil))

try await NIOHTTPServerTests.validateResponse(
inbound,
expectedHead: [NIOHTTPServerTests.responseHead(status: .ok, for: .http1_1)],
expectedBody: [],
expectStreamEnd: false
)

responseReceived()
}
}
) { serverAddress in
await withThrowingTaskGroup { group in
for _ in 0..<numConnections {
group.addTask {
try await TestClientConnection.withConnectedRequestChannel(
configuration: clientConfiguration,
serverAddress: serverAddress
) { inbound, outbound in
try await outbound.write(.testHead(method: .get, for: httpVersion))
try await outbound.write(.end(nil))

try await TestHelpers.validateResponse(
inbound,
expectedHead: [.makeResponse(status: .ok, for: httpVersion)],
expectedBody: [],
expectStreamEnd: false
)

responseReceived()
}
}
}
}
)
}
}
}
}
Loading
Loading