Skip to content

Commit b6fb33b

Browse files
authored
[HTTP2] Test HTTP2 max streams setting is respected. (#465)
1 parent 18a58bb commit b6fb33b

File tree

3 files changed

+245
-0
lines changed

3 files changed

+245
-0
lines changed

Diff for: Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ extension HTTPConnectionPool_HTTP2StateMachineTests {
4040
("testMigrationFromHTTP1ToHTTP2WithAlreadyStartedHTTP1Connections", testMigrationFromHTTP1ToHTTP2WithAlreadyStartedHTTP1Connections),
4141
("testHTTP2toHTTP1Migration", testHTTP2toHTTP1Migration),
4242
("testConnectionIsImmediatelyCreatedAfterBackoffTimerFires", testConnectionIsImmediatelyCreatedAfterBackoffTimerFires),
43+
("testMaxConcurrentStreamsIsRespected", testMaxConcurrentStreamsIsRespected),
4344
]
4445
}
4546
}

Diff for: Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests.swift

+145
Original file line numberDiff line numberDiff line change
@@ -943,4 +943,149 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
943943
XCTAssertEqual(action3.request, .none)
944944
XCTAssertEqual(action3.connection, .none)
945945
}
946+
947+
func testMaxConcurrentStreamsIsRespected() {
948+
let elg = EmbeddedEventLoopGroup(loops: 4)
949+
defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) }
950+
951+
guard var (connections, state) = try? MockConnectionPool.http2(elg: elg, maxConcurrentStreams: 100) else {
952+
return XCTFail("Test setup failed")
953+
}
954+
955+
let generalPurposeConnection = connections.randomParkedConnection()!
956+
var queuer = MockRequestQueuer()
957+
958+
// schedule 1000 requests on the pool. The first 100 will be executed right away. All others
959+
// shall be queued.
960+
for i in 0..<1000 {
961+
let requestEL = elg.next()
962+
let mockRequest = MockHTTPRequest(eventLoop: requestEL)
963+
let request = HTTPConnectionPool.Request(mockRequest)
964+
965+
let executeAction = state.executeRequest(request)
966+
switch i {
967+
case 0:
968+
XCTAssertEqual(executeAction.connection, .cancelTimeoutTimer(generalPurposeConnection.id))
969+
XCTAssertNoThrow(try connections.activateConnection(generalPurposeConnection.id))
970+
XCTAssertEqual(executeAction.request, .executeRequest(request, generalPurposeConnection, cancelTimeout: false))
971+
XCTAssertNoThrow(try connections.execute(mockRequest, on: generalPurposeConnection))
972+
case 1..<100:
973+
XCTAssertEqual(executeAction.request, .executeRequest(request, generalPurposeConnection, cancelTimeout: false))
974+
XCTAssertEqual(executeAction.connection, .none)
975+
XCTAssertNoThrow(try connections.execute(mockRequest, on: generalPurposeConnection))
976+
case 100..<1000:
977+
XCTAssertEqual(executeAction.request, .scheduleRequestTimeout(for: request, on: requestEL))
978+
XCTAssertEqual(executeAction.connection, .none)
979+
XCTAssertNoThrow(try queuer.queue(mockRequest, id: request.id))
980+
default:
981+
XCTFail("Unexpected")
982+
}
983+
}
984+
985+
// let's end processing 500 requests. For every finished request, we will execute another one
986+
// right away
987+
while queuer.count > 500 {
988+
XCTAssertNoThrow(try connections.finishExecution(generalPurposeConnection.id))
989+
let finishAction = state.http2ConnectionStreamClosed(generalPurposeConnection.id)
990+
XCTAssertEqual(finishAction.connection, .none)
991+
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = finishAction.request else {
992+
return XCTFail("Unexpected request action: \(finishAction.request)")
993+
}
994+
guard requests.count == 1, let request = requests.first else {
995+
return XCTFail("Expected to get exactly one request!")
996+
}
997+
let mockRequest = request.__testOnly_wrapped_request()
998+
XCTAssertNoThrow(try queuer.get(request.id, request: mockRequest))
999+
XCTAssertNoThrow(try connections.execute(mockRequest, on: generalPurposeConnection))
1000+
}
1001+
1002+
XCTAssertEqual(queuer.count, 500)
1003+
1004+
// Next the server allows for more concurrent streams
1005+
let newMaxStreams = 200
1006+
XCTAssertNoThrow(try connections.newHTTP2ConnectionSettingsReceived(generalPurposeConnection.id, maxConcurrentStreams: newMaxStreams))
1007+
let newMaxStreamsAction = state.newHTTP2MaxConcurrentStreamsReceived(generalPurposeConnection.id, newMaxStreams: newMaxStreams)
1008+
XCTAssertEqual(newMaxStreamsAction.connection, .none)
1009+
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = newMaxStreamsAction.request else {
1010+
return XCTFail("Unexpected request action after new max concurrent stream setting: \(newMaxStreamsAction.request)")
1011+
}
1012+
XCTAssertEqual(requests.count, 100, "Expected to execute 100 more requests")
1013+
for request in requests {
1014+
let mockRequest = request.__testOnly_wrapped_request()
1015+
XCTAssertNoThrow(try connections.execute(mockRequest, on: generalPurposeConnection))
1016+
XCTAssertNoThrow(try queuer.get(request.id, request: mockRequest))
1017+
}
1018+
1019+
XCTAssertEqual(queuer.count, 400)
1020+
1021+
// let's end processing 100 requests. For every finished request, we will execute another one
1022+
// right away
1023+
while queuer.count > 300 {
1024+
XCTAssertNoThrow(try connections.finishExecution(generalPurposeConnection.id))
1025+
let finishAction = state.http2ConnectionStreamClosed(generalPurposeConnection.id)
1026+
XCTAssertEqual(finishAction.connection, .none)
1027+
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = finishAction.request else {
1028+
return XCTFail("Unexpected request action: \(finishAction.request)")
1029+
}
1030+
guard requests.count == 1, let request = requests.first else {
1031+
return XCTFail("Expected to get exactly one request!")
1032+
}
1033+
let mockRequest = request.__testOnly_wrapped_request()
1034+
XCTAssertNoThrow(try queuer.get(request.id, request: mockRequest))
1035+
XCTAssertNoThrow(try connections.execute(mockRequest, on: generalPurposeConnection))
1036+
}
1037+
1038+
// Next the server allows for fewer concurrent streams
1039+
let fewerMaxStreams = 50
1040+
XCTAssertNoThrow(try connections.newHTTP2ConnectionSettingsReceived(generalPurposeConnection.id, maxConcurrentStreams: fewerMaxStreams))
1041+
let fewerMaxStreamsAction = state.newHTTP2MaxConcurrentStreamsReceived(generalPurposeConnection.id, newMaxStreams: fewerMaxStreams)
1042+
XCTAssertEqual(fewerMaxStreamsAction.connection, .none)
1043+
XCTAssertEqual(fewerMaxStreamsAction.request, .none)
1044+
1045+
// for the next 150 requests that are finished, no new request must be executed.
1046+
for _ in 0..<150 {
1047+
XCTAssertNoThrow(try connections.finishExecution(generalPurposeConnection.id))
1048+
XCTAssertEqual(state.http2ConnectionStreamClosed(generalPurposeConnection.id), .none)
1049+
}
1050+
1051+
XCTAssertEqual(queuer.count, 300)
1052+
1053+
// let's end all remaining requests. For every finished request, we will execute another one
1054+
// right away
1055+
while queuer.count > 0 {
1056+
XCTAssertNoThrow(try connections.finishExecution(generalPurposeConnection.id))
1057+
let finishAction = state.http2ConnectionStreamClosed(generalPurposeConnection.id)
1058+
XCTAssertEqual(finishAction.connection, .none)
1059+
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = finishAction.request else {
1060+
return XCTFail("Unexpected request action: \(finishAction.request)")
1061+
}
1062+
guard requests.count == 1, let request = requests.first else {
1063+
return XCTFail("Expected to get exactly one request!")
1064+
}
1065+
let mockRequest = request.__testOnly_wrapped_request()
1066+
XCTAssertNoThrow(try queuer.get(request.id, request: mockRequest))
1067+
XCTAssertNoThrow(try connections.execute(mockRequest, on: generalPurposeConnection))
1068+
}
1069+
1070+
// Now we only need to drain the remaining 50 requests on the connection
1071+
var timeoutTimerScheduled = false
1072+
for remaining in stride(from: 50, through: 1, by: -1) {
1073+
XCTAssertNoThrow(try connections.finishExecution(generalPurposeConnection.id))
1074+
let finishAction = state.http2ConnectionStreamClosed(generalPurposeConnection.id)
1075+
XCTAssertEqual(finishAction.request, .none)
1076+
switch remaining {
1077+
case 1:
1078+
timeoutTimerScheduled = true
1079+
XCTAssertEqual(finishAction.connection, .scheduleTimeoutTimer(generalPurposeConnection.id, on: generalPurposeConnection.eventLoop))
1080+
XCTAssertNoThrow(try connections.parkConnection(generalPurposeConnection.id))
1081+
case 2...50:
1082+
XCTAssertEqual(finishAction.connection, .none)
1083+
default:
1084+
XCTFail("Unexpected value: \(remaining)")
1085+
}
1086+
}
1087+
XCTAssertTrue(timeoutTimerScheduled)
1088+
XCTAssertNotNil(connections.randomParkedConnection())
1089+
XCTAssertEqual(connections.count, 1)
1090+
}
9461091
}

Diff for: Tests/AsyncHTTPClientTests/Mocks/MockConnectionPool.swift

+99
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ struct MockConnectionPool {
3434
case connectionIsNotStarting
3535
case connectionIsNotExecuting
3636
case connectionDoesNotFulfillEventLoopRequirement
37+
case connectionIsNotActive
38+
case connectionIsNotHTTP2Connection
3739
case connectionDoesNotHaveHTTP2StreamAvailable
3840
case connectionBackoffTimerExists
3941
case connectionBackoffTimerNotFound
@@ -256,6 +258,25 @@ struct MockConnectionPool {
256258
}
257259
}
258260

261+
mutating func newHTTP2SettingsReceived(maxConcurrentStreams newMaxStream: Int) throws {
262+
switch self.state {
263+
case .starting:
264+
throw Errors.connectionIsNotActive
265+
266+
case .http1:
267+
throw Errors.connectionIsNotHTTP2Connection
268+
269+
case .http2(.inUse(_, let used)):
270+
self.state = .http2(.inUse(maxConcurrentStreams: newMaxStream, used: used))
271+
272+
case .http2(.idle(_, let parked, let lastIdle)):
273+
self.state = .http2(.idle(maxConcurrentStreams: newMaxStream, parked: parked, lastIdle: lastIdle))
274+
275+
case .closed:
276+
throw Errors.connectionIsClosed
277+
}
278+
}
279+
259280
mutating func close() throws {
260281
switch self.state {
261282
case .starting:
@@ -378,6 +399,19 @@ struct MockConnectionPool {
378399
self.backoff.insert(connectionID)
379400
}
380401

402+
mutating func newHTTP2ConnectionSettingsReceived(
403+
_ connectionID: Connection.ID,
404+
maxConcurrentStreams: Int
405+
) throws -> Connection {
406+
guard var connection = self.connections[connectionID] else {
407+
throw Errors.connectionNotFound
408+
}
409+
410+
try connection.newHTTP2SettingsReceived(maxConcurrentStreams: maxConcurrentStreams)
411+
self.connections[connection.id] = connection
412+
return .__testOnly_connection(id: connection.id, eventLoop: connection.eventLoop)
413+
}
414+
381415
mutating func connectionBackoffTimerDone(_ connectionID: Connection.ID) throws {
382416
guard self.backoff.remove(connectionID) != nil else {
383417
throw Errors.connectionBackoffTimerNotFound
@@ -561,6 +595,71 @@ extension MockConnectionPool {
561595

562596
return (connections, state)
563597
}
598+
599+
/// Sets up a MockConnectionPool with one established http2 connection
600+
static func http2(
601+
elg: EventLoopGroup,
602+
on eventLoop: EventLoop? = nil,
603+
maxConcurrentStreams: Int = 100
604+
) throws -> (Self, HTTPConnectionPool.StateMachine) {
605+
var state = HTTPConnectionPool.StateMachine(
606+
idGenerator: .init(),
607+
maximumConcurrentHTTP1Connections: 8
608+
)
609+
var connections = MockConnectionPool()
610+
var queuer = MockRequestQueuer()
611+
612+
// 1. Schedule one request to create a connection
613+
614+
let mockRequest = MockHTTPRequest(eventLoop: eventLoop ?? elg.next())
615+
let request = HTTPConnectionPool.Request(mockRequest)
616+
let executeAction = state.executeRequest(request)
617+
618+
guard case .scheduleRequestTimeout(request, on: let waitEL) = executeAction.request, mockRequest.eventLoop === waitEL else {
619+
throw SetupError.expectedRequestToBeAddedToQueue
620+
}
621+
622+
guard case .createConnection(let connectionID, on: let eventLoop) = executeAction.connection else {
623+
throw SetupError.expectedConnectionToBeCreated
624+
}
625+
626+
try connections.createConnection(connectionID, on: eventLoop)
627+
try queuer.queue(mockRequest, id: request.id)
628+
629+
// 2. the connection becomes available
630+
631+
let newConnection = try connections.succeedConnectionCreationHTTP2(connectionID, maxConcurrentStreams: maxConcurrentStreams)
632+
let action = state.newHTTP2ConnectionCreated(newConnection, maxConcurrentStreams: maxConcurrentStreams)
633+
634+
guard case .executeRequestsAndCancelTimeouts([request], newConnection) = action.request else {
635+
throw SetupError.expectedPreviouslyQueuedRequestToBeRunNow
636+
}
637+
638+
guard case .migration(createConnections: let create, closeConnections: [], scheduleTimeout: nil) = action.connection, create.isEmpty else {
639+
throw SetupError.expectedNoConnectionAction
640+
}
641+
642+
guard try queuer.get(request.id, request: request.__testOnly_wrapped_request()) === mockRequest else {
643+
throw SetupError.expectedPreviouslyQueuedRequestToBeRunNow
644+
}
645+
try connections.execute(mockRequest, on: newConnection)
646+
647+
// 3. park connection
648+
649+
try connections.finishExecution(newConnection.id)
650+
651+
let expected: HTTPConnectionPool.StateMachine.ConnectionAction = .scheduleTimeoutTimer(
652+
newConnection.id,
653+
on: newConnection.eventLoop
654+
)
655+
guard state.http2ConnectionStreamClosed(newConnection.id) == .init(request: .none, connection: expected) else {
656+
throw SetupError.expectedConnectionToBeParked
657+
}
658+
659+
try connections.parkConnection(newConnection.id)
660+
661+
return (connections, state)
662+
}
564663
}
565664

566665
/// A request that can be used when testing the `HTTPConnectionPool.StateMachine`

0 commit comments

Comments
 (0)