Files
async-http-client/Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2ConnectionsTest.swift
T
George Barnett 6c5058ee2c Add a control to limit connection reuses (#678)
Motivation:

Sometimes it can be helpful to limit the number of times a connection
can be used before discarding it. AHC has no such support for this at
the moment.

Modifications:

- Add a `maximumUsesPerConnection` configuration option which defaults
  to `nil` (i.e. no limit).
- For HTTP1 we count down uses in the state machine and close the
  connection if it hits zero.
- For HTTP2, each use maps to a stream so we count down remaining uses
  in the state machine which we combine with max concurrent streams to
  limit how many streams are available per connection. We also count
  remaining uses in the HTTP2 idle handler: we treat no remaining uses
  as receiving a GOAWAY frame and notify the pool which then drains the
  streams and replaces the connection.

Result:

Users can control how many times each connection can be used.
2023-04-11 14:49:44 +01:00

727 lines
33 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOCore
import NIOEmbedded
import XCTest
class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
func testCreatingConnections() {
let elg = EmbeddedEventLoopGroup(loops: 4)
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
let el1 = elg.next()
let el2 = elg.next()
// general purpose connection
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el1))
let conn1ID = connections.createNewConnection(on: el1)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el1))
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
XCTAssertEqual(conn1CreatedContext.isIdle, true)
XCTAssert(conn1CreatedContext.eventLoop === el1)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 1)
XCTAssertEqual(leasedConn1, conn1)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
// eventLoop connection
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el2))
let conn2ID = connections.createNewConnection(on: el2)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el2))
let conn2: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn2ID, eventLoop: el2)
let (conn2Index, conn2CreatedContext) = connections.newHTTP2ConnectionEstablished(conn2, maxConcurrentStreams: 100)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
XCTAssertTrue(conn1CreatedContext.isIdle)
XCTAssert(conn2CreatedContext.eventLoop === el2)
let (leasedConn2, leasdConnContext2) = connections.leaseStreams(at: conn2Index, count: 1)
XCTAssertEqual(leasedConn2, conn2)
XCTAssertEqual(leasdConnContext2.wasIdle, true)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el2))
}
func testCreatingConnectionAndFailing() {
let elg = EmbeddedEventLoopGroup(loops: 4)
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
let el1 = elg.next()
let el2 = elg.next()
// general purpose connection
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el1))
let conn1ID = connections.createNewConnection(on: el1)
XCTAssertEqual(conn1ID, 0)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el1))
// connection failed to start. 1. backoff
let backoff1EL = connections.backoffNextConnectionAttempt(conn1ID)
XCTAssert(backoff1EL === el1)
// backoff done. 2. decide what's next
guard let (conn1FailIndex, conn1FailContext) = connections.failConnection(conn1ID) else {
return XCTFail("Expected that the connection is remembered")
}
XCTAssert(conn1FailContext.eventLoop === el1)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el1))
let (replaceConn1ID, replaceConn1EL) = connections.createNewConnectionByReplacingClosedConnection(at: conn1FailIndex)
XCTAssert(replaceConn1EL === el1)
XCTAssertEqual(replaceConn1ID, 1)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el1))
// eventLoop connection
let conn2ID = connections.createNewConnection(on: el2)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el2))
let backoff2EL = connections.backoffNextConnectionAttempt(conn2ID)
XCTAssert(backoff2EL === el2)
guard let (conn2FailIndex, conn2FailContext) = connections.failConnection(conn2ID) else {
return XCTFail("Expected that the connection is remembered")
}
XCTAssert(conn2FailContext.eventLoop === el2)
connections.removeConnection(at: conn2FailIndex)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el2))
}
func testFailConnectionRace() {
let elg = EmbeddedEventLoopGroup(loops: 5)
let el1 = elg.next()
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
// connection is idle
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
_ = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
// connection close is initiated from the pool
XCTAssertEqual(connections.closeConnectionIfIdle(conn1ID), conn1)
// connection will report close event to us even if we have initialled it an we need to tolerate it
XCTAssertNil(connections.failConnection(conn1ID))
}
func testLeaseConnectionOfPreferredButUnavailableEL() {
let elg = EmbeddedEventLoopGroup(loops: 5)
let el1 = elg.next()
let el2 = elg.next()
let el3 = elg.next()
let el4 = elg.next()
let el5 = elg.next()
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
for el in [el1, el2, el3, el4] {
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el))
let connID = connections.createNewConnection(on: el)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el))
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el)
let (_, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn, maxConcurrentStreams: 100)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
XCTAssertEqual(conn1CreatedContext.isIdle, true)
XCTAssert(conn1CreatedContext.eventLoop === el)
}
XCTAssertNotNil(connections.leaseStream(onPreferred: el5))
}
func testLeaseConnectionOnRequiredButUnavailableEL() {
let elg = EmbeddedEventLoopGroup(loops: 5)
let el1 = elg.next()
let el2 = elg.next()
let el3 = elg.next()
let el4 = elg.next()
let el5 = elg.next()
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
for el in [el1, el2, el3, el4] {
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el))
let connID = connections.createNewConnection(on: el)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el))
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el)
let (_, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn, maxConcurrentStreams: 100)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
XCTAssertEqual(conn1CreatedContext.isIdle, true)
XCTAssert(conn1CreatedContext.eventLoop === el)
}
XCTAssertNil(connections.leaseStream(onRequired: el5))
}
func testCloseConnectionIfIdle() {
let elg = EmbeddedEventLoopGroup(loops: 5)
let el1 = elg.next()
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
// connection is idle
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
_ = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
XCTAssertEqual(connections.closeConnectionIfIdle(conn1ID), conn1)
// connection is not idle
let conn2ID = connections.createNewConnection(on: el1)
let conn2: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn2ID, eventLoop: el1)
let (conn2Index, _) = connections.newHTTP2ConnectionEstablished(conn2, maxConcurrentStreams: 100)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn2Index, count: 1)
XCTAssertEqual(leasedConn1, conn2)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
XCTAssertNil(connections.closeConnectionIfIdle(conn2ID))
}
func testCloseConnectionIfIdleButLeasedRaceCondition() {
let elg = EmbeddedEventLoopGroup(loops: 5)
let el1 = elg.next()
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
// connection is idle
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
_ = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
// connection is leased
guard let (leasedConn, leaseContext) = connections.leaseStream(onPreferred: el1) else {
return XCTFail("lease unexpectedly failed")
}
XCTAssertEqual(leasedConn, conn1)
XCTAssertEqual(leaseContext.wasIdle, true)
// timeout arrives minimal to late
XCTAssertEqual(connections.closeConnectionIfIdle(conn1ID), nil)
}
func testCloseConnectionIfIdleButClosedRaceCondition() {
let elg = EmbeddedEventLoopGroup(loops: 5)
let el1 = elg.next()
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
// connection is idle
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
_ = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
_ = connections.failConnection(conn1ID)
// timeout arrives minimal to late
XCTAssertEqual(connections.closeConnectionIfIdle(conn1ID), nil)
}
func testCloseConnectionIfIdleRace() {
let elg = EmbeddedEventLoopGroup(loops: 5)
let el1 = elg.next()
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
// connection is idle
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
_ = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
// we lease it just before timeout
guard let (leasedConn, leaseContext) = connections.leaseStream(onRequired: el1) else {
return XCTFail("lease unexpectedly failed")
}
XCTAssertEqual(leasedConn, conn1)
XCTAssertEqual(leaseContext.wasIdle, true)
// timeout arrives minimal to late
XCTAssertEqual(connections.closeConnectionIfIdle(conn1ID), nil)
}
func testShutdown() {
let elg = EmbeddedEventLoopGroup(loops: 6)
let el1 = elg.next()
let el2 = elg.next()
let el3 = elg.next()
let el4 = elg.next()
let el5 = elg.next()
let el6 = elg.next()
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
for el in [el1, el2, el3, el4] {
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el))
let connID = connections.createNewConnection(on: el)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el))
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el)
let (_, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn, maxConcurrentStreams: 100)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
XCTAssertEqual(conn1CreatedContext.isIdle, true)
XCTAssert(conn1CreatedContext.eventLoop === el)
}
XCTAssertEqual(connections.stats.backingOffConnections, 0)
XCTAssertEqual(connections.stats.leasedStreams, 0)
XCTAssertEqual(connections.stats.availableStreams, 400)
XCTAssertEqual(connections.stats.idleConnections, 4)
// connection is leased
guard let (leasedConn, leaseContext) = connections.leaseStream(onPreferred: el1) else {
return XCTFail("Expected to be able to lease a connection")
}
XCTAssertEqual(leasedConn, .__testOnly_connection(id: 0, eventLoop: el1))
XCTAssertEqual(leaseContext.wasIdle, true)
XCTAssertEqual(connections.stats.backingOffConnections, 0)
XCTAssertEqual(connections.stats.leasedStreams, 1)
XCTAssertEqual(connections.stats.availableStreams, 399)
XCTAssertEqual(connections.stats.idleConnections, 3)
// start another connection that fails
let backingOffID = connections.createNewConnection(on: el5)
XCTAssert(connections.backoffNextConnectionAttempt(backingOffID) === el5)
// start another connection
let startingID = connections.createNewConnection(on: el6)
let context = connections.shutdown()
XCTAssertEqual(context.close.count, 3)
XCTAssertEqual(context.cancel, [leasedConn])
XCTAssertEqual(context.connectBackoff, [backingOffID])
XCTAssertEqual(connections.stats.idleConnections, 0)
XCTAssertEqual(connections.stats.backingOffConnections, 0)
XCTAssertEqual(connections.stats.leasedStreams, 1)
XCTAssertEqual(connections.stats.availableStreams, 99)
XCTAssertEqual(connections.stats.startingConnections, 1)
XCTAssertFalse(connections.isEmpty)
let (releaseIndex, _) = connections.releaseStream(leasedConn.id)
XCTAssertEqual(connections.closeConnection(at: releaseIndex), leasedConn)
XCTAssertFalse(connections.isEmpty)
guard let (failIndex, _) = connections.failConnection(startingID) else {
return XCTFail("Expected that the connection is remembered")
}
connections.removeConnection(at: failIndex)
XCTAssertTrue(connections.isEmpty)
}
func testLeasingAllConnections() {
let elg = EmbeddedEventLoopGroup(loops: 4)
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
let el1 = elg.next()
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 100)
XCTAssertEqual(leasedConn1, conn1)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
XCTAssertNil(connections.leaseStream(onRequired: el1), "should not be able to lease stream because they are all already leased")
let (_, releaseContext) = connections.releaseStream(conn1ID)
XCTAssertFalse(releaseContext.isIdle)
XCTAssertEqual(releaseContext.availableStreams, 1)
guard let (leasedConn, leaseContext) = connections.leaseStream(onRequired: el1) else {
return XCTFail("lease unexpectedly failed")
}
XCTAssertEqual(leasedConn, conn1)
XCTAssertEqual(leaseContext.wasIdle, false)
XCTAssertNil(connections.leaseStream(onRequired: el1), "should not be able to lease stream because they are all already leased")
}
func testGoAway() {
let elg = EmbeddedEventLoopGroup(loops: 4)
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
let el1 = elg.next()
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 10)
XCTAssertEqual(conn1CreatedContext.availableStreams, 10)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 2)
XCTAssertEqual(leasedConn1, conn1)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
XCTAssertTrue(connections.goAwayReceived(conn1ID)?.eventLoop === el1)
XCTAssertEqual(
connections.stats,
.init(
startingConnections: 0,
backingOffConnections: 0,
idleConnections: 0,
availableConnections: 0,
drainingConnections: 1,
leasedStreams: 2,
availableStreams: 0
)
)
XCTAssertNil(connections.leaseStream(onRequired: el1), "we should not be able to lease a stream because the connection is draining")
// a server can potentially send more than one connection go away and we should not crash
XCTAssertTrue(connections.goAwayReceived(conn1ID)?.eventLoop === el1)
XCTAssertEqual(
connections.stats,
.init(
startingConnections: 0,
backingOffConnections: 0,
idleConnections: 0,
availableConnections: 0,
drainingConnections: 1,
leasedStreams: 2,
availableStreams: 0
)
)
// release a connection
let (_, release1Context) = connections.releaseStream(conn1ID)
XCTAssertFalse(release1Context.isIdle)
XCTAssertEqual(release1Context.availableStreams, 0)
XCTAssertEqual(
connections.stats,
.init(
startingConnections: 0,
backingOffConnections: 0,
idleConnections: 0,
availableConnections: 0,
drainingConnections: 1,
leasedStreams: 1,
availableStreams: 0
)
)
// release last connection
let (_, release2Context) = connections.releaseStream(conn1ID)
XCTAssertFalse(release2Context.isIdle)
XCTAssertEqual(release2Context.availableStreams, 0)
XCTAssertEqual(
connections.stats,
.init(
startingConnections: 0,
backingOffConnections: 0,
idleConnections: 0,
availableConnections: 0,
drainingConnections: 1,
leasedStreams: 0,
availableStreams: 0
)
)
}
func testNewMaxConcurrentStreamsSetting() {
let elg = EmbeddedEventLoopGroup(loops: 4)
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
let el1 = elg.next()
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 1)
XCTAssertEqual(conn1CreatedContext.availableStreams, 1)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 1)
XCTAssertEqual(leasedConn1, conn1)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
XCTAssertNil(connections.leaseStream(onRequired: el1), "all streams are in use")
guard let (_, newSettingsContext1) = connections.newHTTP2MaxConcurrentStreamsReceived(conn1ID, newMaxStreams: 2) else {
return XCTFail("Expected to get a new settings context")
}
XCTAssertEqual(newSettingsContext1.availableStreams, 1)
XCTAssertTrue(newSettingsContext1.eventLoop === el1)
XCTAssertFalse(newSettingsContext1.isIdle)
guard let (leasedConn2, leaseContext2) = connections.leaseStream(onRequired: el1) else {
return XCTFail("lease unexpectedly failed")
}
XCTAssertEqual(leasedConn2, conn1)
XCTAssertEqual(leaseContext2.wasIdle, false)
guard let (_, newSettingsContext2) = connections.newHTTP2MaxConcurrentStreamsReceived(conn1ID, newMaxStreams: 1) else {
return XCTFail("Expected to get a new settings context")
}
XCTAssertEqual(newSettingsContext2.availableStreams, 0)
XCTAssertTrue(newSettingsContext2.eventLoop === el1)
XCTAssertFalse(newSettingsContext2.isIdle)
// release a connection
let (_, release1Context) = connections.releaseStream(conn1ID)
XCTAssertFalse(release1Context.isIdle)
XCTAssertEqual(release1Context.availableStreams, 0)
XCTAssertNil(connections.leaseStream(onRequired: el1), "all streams are in use")
// release a connection
let (_, release2Context) = connections.releaseStream(conn1ID)
XCTAssertTrue(release2Context.isIdle)
XCTAssertEqual(release2Context.availableStreams, 1)
guard let (leasedConn3, leaseContext3) = connections.leaseStream(onRequired: el1) else {
return XCTFail("lease unexpectedly failed")
}
XCTAssertEqual(leasedConn3, conn1)
XCTAssertEqual(leaseContext3.wasIdle, true)
}
func testEventsAfterConnectionIsClosed() {
let elg = EmbeddedEventLoopGroup(loops: 2)
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
let el1 = elg.next()
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 1)
XCTAssertEqual(conn1CreatedContext.availableStreams, 1)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 1)
XCTAssertEqual(leasedConn1, conn1)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
XCTAssertNil(connections.leaseStream(onRequired: el1), "all streams are in use")
let (_, releaseContext) = connections.releaseStream(conn1ID)
XCTAssertTrue(releaseContext.eventLoop === el1)
XCTAssertEqual(releaseContext.availableStreams, 1)
XCTAssertEqual(releaseContext.connectionID, conn1ID)
XCTAssertEqual(releaseContext.isIdle, true)
// schedule timeout... this should remove the connection from http2Connections
XCTAssertEqual(connections.closeConnectionIfIdle(conn1ID), conn1)
// events race with the complete shutdown
XCTAssertNil(connections.newHTTP2MaxConcurrentStreamsReceived(conn1ID, newMaxStreams: 2))
XCTAssertNil(connections.goAwayReceived(conn1ID))
// finally close event
XCTAssertNil(connections.failConnection(conn1ID))
}
func testLeaseOnPreferredEventLoopWithoutAnyAvailable() {
let elg = EmbeddedEventLoopGroup(loops: 4)
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
let el1 = elg.next()
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 1)
XCTAssertEqual(conn1CreatedContext.availableStreams, 1)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 1)
XCTAssertEqual(leasedConn1, conn1)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
XCTAssertNil(connections.leaseStream(onPreferred: el1), "all streams are in use")
}
func testMigrationFromHTTP1() {
let elg = EmbeddedEventLoopGroup(loops: 4)
var connections = HTTPConnectionPool.HTTP2Connections(generator: .init(), maximumConnectionUses: nil)
let el1 = elg.next()
let el2 = elg.next()
let conn1ID: HTTPConnectionPool.Connection.ID = 1
let conn2ID: HTTPConnectionPool.Connection.ID = 2
connections.migrateFromHTTP1(
starting: [(conn1ID, el1)],
backingOff: [(conn2ID, el2)]
)
XCTAssertTrue(connections.createConnectionsAfterMigrationIfNeeded(
requiredEventLoopsOfPendingRequests: [el1, el2]
).isEmpty)
XCTAssertEqual(
connections.stats,
.init(
startingConnections: 1,
backingOffConnections: 1,
idleConnections: 0,
availableConnections: 0,
drainingConnections: 0,
leasedStreams: 0,
availableStreams: 0
)
)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 2)
XCTAssertEqual(leasedConn1, conn1)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
XCTAssertEqual(
connections.stats,
.init(
startingConnections: 0,
backingOffConnections: 1,
idleConnections: 0,
availableConnections: 1,
drainingConnections: 0,
leasedStreams: 2,
availableStreams: 98
)
)
}
func testMigrationToHTTP1() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP2Connections(generator: generator, maximumConnectionUses: nil)
let el1 = elg.next()
let el2 = elg.next()
let el3 = elg.next()
let el4 = elg.next()
let conn1ID = generator.next()
let conn2ID = generator.next()
let conn3ID = generator.next()
let conn4ID = generator.next()
connections.migrateFromHTTP1(
starting: [(conn1ID, el1), (conn2ID, el2), (conn3ID, el3)],
backingOff: [(conn4ID, el4)]
)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 2)
XCTAssertEqual(leasedConn1, conn1)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
let conn2: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn2ID, eventLoop: el2)
let (_, conn2CreatedContext) = connections.newHTTP2ConnectionEstablished(conn2, maxConcurrentStreams: 100)
XCTAssertEqual(conn2CreatedContext.availableStreams, 100)
XCTAssertEqual(
connections.stats,
.init(
startingConnections: 1,
backingOffConnections: 1,
idleConnections: 1,
availableConnections: 2,
drainingConnections: 0,
leasedStreams: 2,
availableStreams: 198
)
)
let migrationContext = connections.migrateToHTTP1()
XCTAssertEqual(migrationContext.close, [conn2])
XCTAssertEqual(migrationContext.starting.map { $0.0 }, [conn3ID])
XCTAssertEqual(migrationContext.starting.map { $0.1.id }, [el3.id])
XCTAssertEqual(migrationContext.backingOff.map { $0.0 }, [conn4ID])
XCTAssertEqual(migrationContext.backingOff.map { $0.1.id }, [el4.id])
XCTAssertEqual(
connections.stats,
.init(
startingConnections: 0,
backingOffConnections: 0,
idleConnections: 0,
availableConnections: 1,
drainingConnections: 0,
leasedStreams: 2,
availableStreams: 98
)
)
}
func testMigrationFromHTTP1WithPendingRequestsWithRequiredEventLoop() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP2Connections(generator: generator, maximumConnectionUses: nil)
let el1 = elg.next()
let el2 = elg.next()
let el3 = elg.next()
let conn1ID = generator.next()
let conn2ID = generator.next()
connections.migrateFromHTTP1(
starting: [(conn1ID, el1)],
backingOff: [(conn2ID, el2)]
)
let newConnections = connections.createConnectionsAfterMigrationIfNeeded(
requiredEventLoopsOfPendingRequests: [el1, el2, el3]
)
XCTAssertEqual(newConnections.count, 1)
guard let (conn3ID, eventLoop) = newConnections.first else {
return XCTFail("expected to start a new connection")
}
XCTAssertTrue(eventLoop === el3)
let conn3: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn3ID, eventLoop: el3)
let (_, context) = connections.newHTTP2ConnectionEstablished(conn3, maxConcurrentStreams: 100)
XCTAssertEqual(context.availableStreams, 100)
XCTAssertEqual(context.eventLoop.id, el3.id)
XCTAssertEqual(context.isIdle, true)
XCTAssertEqual(context.connectionID, conn3ID)
}
func testMigrationFromHTTP1WithAlreadyEstablishedHTTP2Connection() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP2Connections(generator: generator, maximumConnectionUses: nil)
let el1 = elg.next()
let el2 = elg.next()
let el3 = elg.next()
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (index, _) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
_ = connections.leaseStreams(at: index, count: 1)
let conn2ID = generator.next()
let conn3ID = generator.next()
connections.migrateFromHTTP1(
starting: [(conn2ID, el2)],
backingOff: [(conn3ID, el3)]
)
XCTAssertTrue(connections.createConnectionsAfterMigrationIfNeeded(
requiredEventLoopsOfPendingRequests: [el1, el2, el3]
).isEmpty, "we still have an active connection for el1 and should not create a new one")
guard let (leasedConn, _) = connections.leaseStream(onRequired: el1) else {
return XCTFail("could not lease stream on el1")
}
XCTAssertEqual(leasedConn, conn1)
}
}