mirror of
https://github.com/swift-server/async-http-client.git
synced 2026-05-03 07:32:29 +00:00
3c45dbde2d
### Motivation When creating a connection, we wrongfully assumed that `failedToCreateNewConnection` will always be called before `http*ConnectionClosed` in the `HTTPConnectionPoolStateMachine`. However this is far from correct. In NIO Futures are fulfilled before `ChannelHandler` callbacks. Ordering in futures should not be assumed in such a complex project. ### Change We change the `http*ConnectionClosed` methods to be noops, if the connection is in the starting state. We instead wait for the `failedToCreateNewConnection` to create backoff timers and friends. rdar://164674912 --------- Co-authored-by: George Barnett <gbarnett@apple.com>
729 lines
30 KiB
Swift
729 lines
30 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import NIOCore
|
|
import NIOEmbedded
|
|
import XCTest
|
|
|
|
@testable import AsyncHTTPClient
|
|
|
|
class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
|
|
func testCreatingConnections() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
|
|
// general purpose connection
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el1), 0)
|
|
let conn1ID = connections.createNewConnection(on: el1)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 1)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el1), 0)
|
|
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
|
|
let (conn1Index, conn1CreatedContext) = connections.newHTTP1ConnectionEstablished(conn1)
|
|
XCTAssertEqual(conn1CreatedContext.use, .generalPurpose)
|
|
XCTAssert(conn1CreatedContext.eventLoop === el1)
|
|
XCTAssertEqual(connections.leaseConnection(at: conn1Index), conn1)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
|
|
// eventLoop connection
|
|
let conn2ID = connections.createNewOverflowConnection(on: el2)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el2), 1)
|
|
let conn2: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn2ID, eventLoop: el2)
|
|
let (conn2Index, conn2CreatedContext) = connections.newHTTP1ConnectionEstablished(conn2)
|
|
XCTAssertEqual(conn2CreatedContext.use, .eventLoop(el2))
|
|
XCTAssert(conn2CreatedContext.eventLoop === el2)
|
|
XCTAssertEqual(connections.leaseConnection(at: conn2Index), conn2)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el2), 0)
|
|
}
|
|
|
|
func testCreatingConnectionAndFailing() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
|
|
// general purpose connection
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el1), 0)
|
|
let conn1ID = connections.createNewConnection(on: el1)
|
|
XCTAssertEqual(conn1ID, 0)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 1)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el1), 0)
|
|
// 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)
|
|
XCTAssertEqual(conn1FailContext.use, .generalPurpose)
|
|
XCTAssertEqual(conn1FailContext.connectionsStartingForUseCase, 0)
|
|
let (replaceConn1ID, replaceConn1EL) = connections.replaceConnection(at: conn1FailIndex)
|
|
XCTAssert(replaceConn1EL === el1)
|
|
XCTAssertEqual(replaceConn1ID, 1)
|
|
|
|
// eventLoop connection
|
|
let conn2ID = connections.createNewOverflowConnection(on: el2)
|
|
// the replacement connection is starting
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 1)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el2), 1)
|
|
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)
|
|
XCTAssertEqual(conn2FailContext.use, .eventLoop(el2))
|
|
XCTAssertEqual(conn2FailContext.connectionsStartingForUseCase, 0)
|
|
connections.removeConnection(at: conn2FailIndex)
|
|
// the replacement connection is still starting
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 1)
|
|
}
|
|
|
|
func testLeaseConnectionOnPreferredAndAvailableEL() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
let el3 = elg.next()
|
|
let el4 = elg.next()
|
|
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
for el in [el1, el2, el3, el4] {
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el), 0)
|
|
let connID = connections.createNewConnection(on: el)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 1)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el), 0)
|
|
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el)
|
|
let (_, connCreatedContext) = connections.newHTTP1ConnectionEstablished(conn)
|
|
XCTAssertEqual(connCreatedContext.use, .generalPurpose)
|
|
XCTAssert(connCreatedContext.eventLoop === el)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
}
|
|
|
|
let connection = connections.leaseConnection(onPreferred: el1)
|
|
XCTAssertEqual(connection, .__testOnly_connection(id: 0, eventLoop: el1))
|
|
}
|
|
|
|
func testLeaseConnectionOnPreferredButUnavailableEL() {
|
|
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.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
for el in [el1, el2, el3, el4] {
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el), 0)
|
|
let connID = connections.createNewConnection(on: el)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 1)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el), 0)
|
|
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el)
|
|
let (_, connCreatedContext) = connections.newHTTP1ConnectionEstablished(conn)
|
|
XCTAssertEqual(connCreatedContext.use, .generalPurpose)
|
|
XCTAssert(connCreatedContext.eventLoop === el)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
}
|
|
|
|
let connection = connections.leaseConnection(onPreferred: el5)
|
|
XCTAssertEqual(connection, .__testOnly_connection(id: 3, eventLoop: el4))
|
|
}
|
|
|
|
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.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
for el in [el1, el2, el3, el4] {
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el), 0)
|
|
let connID = connections.createNewConnection(on: el)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 1)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el), 0)
|
|
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el)
|
|
let (_, connCreatedContext) = connections.newHTTP1ConnectionEstablished(conn)
|
|
XCTAssertEqual(connCreatedContext.use, .generalPurpose)
|
|
XCTAssert(connCreatedContext.eventLoop === el)
|
|
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
|
|
}
|
|
|
|
let connection = connections.leaseConnection(onRequired: el5)
|
|
XCTAssertEqual(connection, .none)
|
|
}
|
|
|
|
func testLeaseConnectionOnRequiredAndAvailableEL() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 2)
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
for el in [el1, el1, el1, el1, el2] {
|
|
let connID = connections.createNewConnection(on: el)
|
|
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el)
|
|
_ = connections.newHTTP1ConnectionEstablished(conn)
|
|
}
|
|
|
|
// 1. get el from general pool, even though el is required!
|
|
guard let lease1 = connections.leaseConnection(onRequired: el1) else {
|
|
return XCTFail("Expected to get a connection at this point.")
|
|
}
|
|
// the last created connection on the correct el is the shortest amount idle. we should use this
|
|
XCTAssertEqual(lease1, .__testOnly_connection(id: 3, eventLoop: el1))
|
|
_ = connections.releaseConnection(lease1.id)
|
|
|
|
// 2. create specialized el connection
|
|
let connID5 = connections.createNewOverflowConnection(on: el1)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el1), 1)
|
|
let conn5: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID5, eventLoop: el1)
|
|
_ = connections.newHTTP1ConnectionEstablished(conn5)
|
|
XCTAssertEqual(connections.startingEventLoopConnections(on: el1), 0)
|
|
|
|
// 3. get el from specialized pool, since it is the newest!
|
|
guard let lease2 = connections.leaseConnection(onRequired: el1) else {
|
|
return XCTFail("Expected to get a connection at this point.")
|
|
}
|
|
XCTAssertEqual(lease2, conn5)
|
|
_ = connections.releaseConnection(lease2.id)
|
|
|
|
// 4. create another general purpose connection on the correct el
|
|
let connID6 = connections.createNewConnection(on: el1)
|
|
let conn6: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID6, eventLoop: el1)
|
|
_ = connections.newHTTP1ConnectionEstablished(conn6)
|
|
|
|
// 5. get el from general pool, since it is the newest!
|
|
guard let lease3 = connections.leaseConnection(onRequired: el1) else {
|
|
return XCTFail("Expected to get a connection at this point.")
|
|
}
|
|
// the last created connection is the shortest amount idle. we should use this
|
|
XCTAssertEqual(lease3, conn6)
|
|
|
|
_ = connections.releaseConnection(lease3.id)
|
|
}
|
|
|
|
func testCloseConnectionIfIdle() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 1)
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
|
|
// connection is idle
|
|
let conn1ID = connections.createNewConnection(on: el1)
|
|
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
|
|
_ = connections.newHTTP1ConnectionEstablished(conn1)
|
|
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.newHTTP1ConnectionEstablished(conn2)
|
|
XCTAssertEqual(connections.leaseConnection(at: conn2Index), conn2)
|
|
XCTAssertNil(connections.closeConnectionIfIdle(conn2ID))
|
|
}
|
|
|
|
func testCloseConnectionIfIdleButLeasedRaceCondition() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 1)
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
|
|
// connection is idle
|
|
let connID = connections.createNewConnection(on: el1)
|
|
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el1)
|
|
_ = connections.newHTTP1ConnectionEstablished(conn)
|
|
|
|
// connection is leased
|
|
let lease = connections.leaseConnection(onPreferred: el1)
|
|
XCTAssertEqual(lease, conn)
|
|
|
|
// timeout arrives minimal to late
|
|
XCTAssertEqual(connections.closeConnectionIfIdle(connID), nil)
|
|
}
|
|
|
|
func testCloseConnectionIfIdleButClosedRaceCondition() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 1)
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
|
|
// connection is idle
|
|
let connID = connections.createNewConnection(on: el1)
|
|
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el1)
|
|
_ = connections.newHTTP1ConnectionEstablished(conn)
|
|
_ = connections.failConnection(connID)
|
|
|
|
// timeout arrives minimal to late
|
|
XCTAssertEqual(connections.closeConnectionIfIdle(connID), nil)
|
|
}
|
|
|
|
func testShutdown() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
let el3 = elg.next()
|
|
let el4 = elg.next()
|
|
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: .init(),
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
for el in [el1, el2, el3, el4] {
|
|
let connID = connections.createNewConnection(on: el)
|
|
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el)
|
|
let (_, connContext) = connections.newHTTP1ConnectionEstablished(conn)
|
|
XCTAssertEqual(connContext.use, .generalPurpose)
|
|
XCTAssert(connContext.eventLoop === el)
|
|
}
|
|
|
|
XCTAssertEqual(connections.stats.backingOff, 0)
|
|
XCTAssertEqual(connections.stats.leased, 0)
|
|
XCTAssertEqual(connections.stats.idle, 4)
|
|
|
|
// connection is leased
|
|
guard let lease = connections.leaseConnection(onPreferred: el1) else {
|
|
return XCTFail("Expected to be able to lease a connection")
|
|
}
|
|
XCTAssertEqual(lease, .__testOnly_connection(id: 0, eventLoop: el1))
|
|
|
|
XCTAssertEqual(connections.stats.leased, 1)
|
|
XCTAssertEqual(connections.stats.idle, 3)
|
|
|
|
// start another connection that fails
|
|
let backingOffID = connections.createNewConnection(on: el1)
|
|
XCTAssert(connections.backoffNextConnectionAttempt(backingOffID) === el1)
|
|
|
|
// start another connection
|
|
let startingID = connections.createNewConnection(on: el2)
|
|
|
|
let context = connections.shutdown()
|
|
XCTAssertEqual(context.close.count, 3)
|
|
XCTAssertEqual(context.cancel, [lease])
|
|
XCTAssertEqual(context.connectBackoff, [backingOffID])
|
|
|
|
XCTAssertEqual(connections.stats.idle, 0)
|
|
XCTAssertEqual(connections.stats.backingOff, 0)
|
|
XCTAssertEqual(connections.stats.leased, 1)
|
|
XCTAssertEqual(connections.stats.connecting, 1)
|
|
XCTAssertFalse(connections.isEmpty)
|
|
|
|
let (releaseIndex, _) = connections.releaseConnection(lease.id)
|
|
XCTAssertEqual(connections.closeConnection(at: releaseIndex), lease)
|
|
XCTAssertFalse(connections.isEmpty)
|
|
|
|
let backoffEL = connections.backoffNextConnectionAttempt(startingID)
|
|
XCTAssertIdentical(backoffEL, el2)
|
|
guard let (failIndex, _) = connections.failConnection(startingID) else {
|
|
return XCTFail("Expected that the connection is remembered")
|
|
}
|
|
connections.removeConnection(at: failIndex)
|
|
XCTAssertTrue(connections.isEmpty)
|
|
}
|
|
|
|
func testMigrationFromHTTP2() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
let generator = HTTPConnectionPool.Connection.ID.Generator()
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: generator,
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
|
|
let conn1ID = generator.next()
|
|
let conn2ID = generator.next()
|
|
|
|
connections.migrateFromHTTP2(
|
|
starting: [(conn1ID, el1)],
|
|
backingOff: [(conn2ID, el2)]
|
|
)
|
|
let newConnections = connections.createConnectionsAfterMigrationIfNeeded(
|
|
requiredEventLoopOfPendingRequests: [],
|
|
generalPurposeRequestCountGroupedByPreferredEventLoop: [(el1, 1), (el2, 1)]
|
|
)
|
|
|
|
XCTAssertTrue(newConnections.isEmpty)
|
|
|
|
let stats = connections.stats
|
|
XCTAssertEqual(stats.idle, 0)
|
|
XCTAssertEqual(stats.leased, 0)
|
|
XCTAssertEqual(stats.connecting, 1)
|
|
XCTAssertEqual(stats.backingOff, 1)
|
|
}
|
|
|
|
func testMigrationFromHTTP2WithPendingRequestsWithRequiredEventLoop() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
let generator = HTTPConnectionPool.Connection.ID.Generator()
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: generator,
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
let el3 = elg.next()
|
|
|
|
let conn1ID = generator.next()
|
|
let conn2ID = generator.next()
|
|
|
|
connections.migrateFromHTTP2(
|
|
starting: [(conn1ID, el1)],
|
|
backingOff: [(conn2ID, el2)]
|
|
)
|
|
let newConnections = connections.createConnectionsAfterMigrationIfNeeded(
|
|
requiredEventLoopOfPendingRequests: [(el3, 1)],
|
|
generalPurposeRequestCountGroupedByPreferredEventLoop: []
|
|
)
|
|
XCTAssertEqual(newConnections.count, 1)
|
|
XCTAssertEqual(newConnections.first?.1.id, el3.id)
|
|
|
|
guard let conn3ID = newConnections.first?.0 else {
|
|
return XCTFail("expected to start a new connection")
|
|
}
|
|
|
|
let stats = connections.stats
|
|
XCTAssertEqual(stats.idle, 0)
|
|
XCTAssertEqual(stats.leased, 0)
|
|
XCTAssertEqual(stats.connecting, 2)
|
|
XCTAssertEqual(stats.backingOff, 1)
|
|
|
|
let conn3: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn3ID, eventLoop: el3)
|
|
let (_, context) = connections.newHTTP1ConnectionEstablished(conn3)
|
|
XCTAssertEqual(context.use, .eventLoop(el3))
|
|
XCTAssertTrue(context.eventLoop === el3)
|
|
}
|
|
|
|
func testMigrationFromHTTP2WithPendingRequestsWithRequiredEventLoopSameAsStartingConnections() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
let generator = HTTPConnectionPool.Connection.ID.Generator()
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: generator,
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
|
|
let conn1ID = generator.next()
|
|
let conn2ID = generator.next()
|
|
|
|
connections.migrateFromHTTP2(
|
|
starting: [(conn1ID, el1)],
|
|
backingOff: [(conn2ID, el2)]
|
|
)
|
|
|
|
let stats = connections.stats
|
|
XCTAssertEqual(stats.idle, 0)
|
|
XCTAssertEqual(stats.leased, 0)
|
|
XCTAssertEqual(stats.connecting, 1)
|
|
XCTAssertEqual(stats.backingOff, 1)
|
|
|
|
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
|
|
let (_, context) = connections.newHTTP1ConnectionEstablished(conn1)
|
|
XCTAssertEqual(context.use, .generalPurpose)
|
|
XCTAssertTrue(context.eventLoop === el1)
|
|
}
|
|
|
|
func testMigrationFromHTTP2WithPendingRequestsWithPreferredEventLoop() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
let generator = HTTPConnectionPool.Connection.ID.Generator()
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: generator,
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
let el3 = elg.next()
|
|
|
|
let conn1ID = generator.next()
|
|
let conn2ID = generator.next()
|
|
|
|
connections.migrateFromHTTP2(
|
|
starting: [(conn1ID, el1)],
|
|
backingOff: [(conn2ID, el2)]
|
|
)
|
|
let newConnections = connections.createConnectionsAfterMigrationIfNeeded(
|
|
requiredEventLoopOfPendingRequests: [],
|
|
generalPurposeRequestCountGroupedByPreferredEventLoop: [(el3, 3)]
|
|
)
|
|
XCTAssertEqual(newConnections.count, 1)
|
|
XCTAssertEqual(newConnections.first?.1.id, el3.id)
|
|
|
|
guard let conn3ID = newConnections.first?.0 else {
|
|
return XCTFail("expected to start a new connection")
|
|
}
|
|
|
|
let stats = connections.stats
|
|
XCTAssertEqual(stats.idle, 0)
|
|
XCTAssertEqual(stats.leased, 0)
|
|
XCTAssertEqual(stats.connecting, 2)
|
|
XCTAssertEqual(stats.backingOff, 1)
|
|
|
|
let conn3: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn3ID, eventLoop: el3)
|
|
let (_, context) = connections.newHTTP1ConnectionEstablished(conn3)
|
|
XCTAssertEqual(context.use, .generalPurpose)
|
|
XCTAssertTrue(context.eventLoop === el3)
|
|
}
|
|
|
|
func testMigrationFromHTTP2WithAlreadyLeasedHTTP1Connection() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
let generator = HTTPConnectionPool.Connection.ID.Generator()
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: generator,
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
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.newHTTP1ConnectionEstablished(conn1)
|
|
_ = connections.leaseConnection(at: index)
|
|
|
|
let conn2ID = generator.next()
|
|
let conn3ID = generator.next()
|
|
|
|
connections.migrateFromHTTP2(
|
|
starting: [(conn2ID, el2)],
|
|
backingOff: [(conn3ID, el3)]
|
|
)
|
|
let newConnections = connections.createConnectionsAfterMigrationIfNeeded(
|
|
requiredEventLoopOfPendingRequests: [],
|
|
generalPurposeRequestCountGroupedByPreferredEventLoop: [(el3, 3)]
|
|
)
|
|
|
|
XCTAssertEqual(newConnections.count, 1)
|
|
XCTAssertEqual(newConnections.first?.1.id, el3.id)
|
|
|
|
guard let conn4ID = newConnections.first?.0 else {
|
|
return XCTFail("expected to start a new connection")
|
|
}
|
|
|
|
let stats = connections.stats
|
|
XCTAssertEqual(stats.idle, 0)
|
|
XCTAssertEqual(stats.leased, 1)
|
|
XCTAssertEqual(stats.connecting, 2)
|
|
XCTAssertEqual(stats.backingOff, 1)
|
|
|
|
let conn3: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn4ID, eventLoop: el3)
|
|
let (_, context) = connections.newHTTP1ConnectionEstablished(conn3)
|
|
XCTAssertEqual(context.use, .generalPurpose)
|
|
XCTAssertTrue(context.eventLoop === el3)
|
|
}
|
|
|
|
func testMigrationFromHTTP2WithMoreStartingConnectionsThanMaximumAllowedConccurentConnections() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
let generator = HTTPConnectionPool.Connection.ID.Generator()
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 2,
|
|
generator: generator,
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
let el3 = elg.next()
|
|
|
|
let conn1ID = generator.next()
|
|
let conn2ID = generator.next()
|
|
let conn3ID = generator.next()
|
|
|
|
connections.migrateFromHTTP2(
|
|
starting: [(conn1ID, el1), (conn2ID, el2), (conn3ID, el3)],
|
|
backingOff: []
|
|
)
|
|
|
|
// first two connections should be added as general purpose connections
|
|
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
|
|
let (_, context1) = connections.newHTTP1ConnectionEstablished(conn1)
|
|
XCTAssertEqual(context1.use, .generalPurpose)
|
|
XCTAssertTrue(context1.eventLoop === el1)
|
|
let conn2: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn2ID, eventLoop: el2)
|
|
let (_, context2) = connections.newHTTP1ConnectionEstablished(conn2)
|
|
XCTAssertEqual(context2.use, .generalPurpose)
|
|
XCTAssertTrue(context2.eventLoop === el2)
|
|
|
|
// additional connection should be added as overflow connection
|
|
let conn3: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn3ID, eventLoop: el3)
|
|
let (_, context3) = connections.newHTTP1ConnectionEstablished(conn3)
|
|
XCTAssertEqual(context3.use, .eventLoop(el3))
|
|
XCTAssertTrue(context3.eventLoop === el3)
|
|
}
|
|
|
|
func testMigrationFromHTTP2StartsEnoghOverflowConnectionsForRequiredEventLoopRequests() {
|
|
let elg = EmbeddedEventLoopGroup(loops: 4)
|
|
let generator = HTTPConnectionPool.Connection.ID.Generator()
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 1,
|
|
generator: generator,
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
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()
|
|
|
|
connections.migrateFromHTTP2(
|
|
starting: [(conn1ID, el1), (conn2ID, el2), (conn3ID, el3)],
|
|
backingOff: []
|
|
)
|
|
|
|
let connectionsToCreate = connections.createConnectionsAfterMigrationIfNeeded(
|
|
requiredEventLoopOfPendingRequests: [(el2, 2), (el3, 1), (el4, 2)],
|
|
generalPurposeRequestCountGroupedByPreferredEventLoop: []
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
connectionsToCreate.map { $0.1.id },
|
|
[el2.id, el4.id, el4.id],
|
|
"should create one connection for el2 and two for el4"
|
|
)
|
|
|
|
for (connID, el) in connectionsToCreate {
|
|
let conn: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID, eventLoop: el)
|
|
let (_, context) = connections.newHTTP1ConnectionEstablished(conn)
|
|
XCTAssertEqual(context.use, .eventLoop(el))
|
|
XCTAssertTrue(context.eventLoop === el)
|
|
}
|
|
}
|
|
|
|
func testMigrationFromHTTP1ToHTTP2AndBackToHTTP1() throws {
|
|
let elg = EmbeddedEventLoopGroup(loops: 2)
|
|
defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) }
|
|
let el1 = elg.next()
|
|
let el2 = elg.next()
|
|
|
|
let generator = HTTPConnectionPool.Connection.ID.Generator()
|
|
var connections = HTTPConnectionPool.HTTP1Connections(
|
|
maximumConcurrentConnections: 8,
|
|
generator: generator,
|
|
maximumConnectionUses: nil,
|
|
preWarmedHTTP1ConnectionCount: 0
|
|
)
|
|
|
|
let connID1 = connections.createNewConnection(on: el1)
|
|
|
|
let context = connections.migrateToHTTP2()
|
|
XCTAssertEqual(
|
|
context,
|
|
.init(
|
|
backingOff: [],
|
|
starting: [(connID1, el1)],
|
|
close: []
|
|
)
|
|
)
|
|
|
|
let connID2 = generator.next()
|
|
|
|
connections.migrateFromHTTP2(
|
|
starting: [(connID2, el2)],
|
|
backingOff: []
|
|
)
|
|
|
|
let conn2: HTTPConnectionPool.Connection = .__testOnly_connection(id: connID2, eventLoop: el2)
|
|
let (_, idleContext) = connections.newHTTP1ConnectionEstablished(conn2)
|
|
XCTAssertEqual(idleContext.use, .generalPurpose)
|
|
XCTAssertEqual(idleContext.eventLoop.id, el2.id)
|
|
}
|
|
}
|
|
|
|
extension HTTPConnectionPool.HTTP1Connections.HTTP1ToHTTP2MigrationContext: Equatable {
|
|
public static func == (lhs: Self, rhs: Self) -> Bool {
|
|
lhs.close == rhs.close && lhs.starting.elementsEqual(rhs.starting, by: { $0.0 == $1.0 && $0.1 === $1.1 })
|
|
&& lhs.backingOff.elementsEqual(rhs.backingOff, by: { $0.0 == $1.0 && $0.1 === $1.1 })
|
|
}
|
|
}
|