Skip to content

Commit 60c25cb

Browse files
committed
Fix Connection Creation Crash
1 parent 4c223e0 commit 60c25cb

File tree

4 files changed

+113
-27
lines changed

4 files changed

+113
-27
lines changed

Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1Connections.swift

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,22 @@ extension HTTPConnectionPool {
166166
}
167167
}
168168

169-
mutating func fail() {
169+
enum FailAction {
170+
case removeConnection
171+
case none
172+
}
173+
174+
mutating func fail() -> FailAction {
170175
switch self.state {
171-
case .starting, .backingOff, .idle, .leased:
176+
case .starting:
177+
// If the connection fails while we are starting it, the fail call raced with
178+
// `failedToConnect` (promises are succeeded or failed before channel handler
179+
// callbacks). let's keep the state in `starting`, so that `failedToConnect` can
180+
// create a backoff timer.
181+
return .none
182+
case .backingOff, .idle, .leased:
172183
self.state = .closed
184+
return .removeConnection
173185
case .closed:
174186
preconditionFailure("Invalid state: \(self.state)")
175187
}
@@ -559,23 +571,28 @@ extension HTTPConnectionPool {
559571
}
560572

561573
let use: ConnectionUse
562-
self.connections[index].fail()
563-
let eventLoop = self.connections[index].eventLoop
564-
let starting: Int
565-
if index < self.overflowIndex {
566-
use = .generalPurpose
567-
starting = self.startingGeneralPurposeConnections
568-
} else {
569-
use = .eventLoop(eventLoop)
570-
starting = self.startingEventLoopConnections(on: eventLoop)
571-
}
574+
switch self.connections[index].fail() {
575+
case .removeConnection:
576+
let eventLoop = self.connections[index].eventLoop
577+
let starting: Int
578+
if index < self.overflowIndex {
579+
use = .generalPurpose
580+
starting = self.startingGeneralPurposeConnections
581+
} else {
582+
use = .eventLoop(eventLoop)
583+
starting = self.startingEventLoopConnections(on: eventLoop)
584+
}
572585

573-
let context = FailedConnectionContext(
574-
eventLoop: eventLoop,
575-
use: use,
576-
connectionsStartingForUseCase: starting
577-
)
578-
return (index, context)
586+
let context = FailedConnectionContext(
587+
eventLoop: eventLoop,
588+
use: use,
589+
connectionsStartingForUseCase: starting
590+
)
591+
return (index, context)
592+
593+
case .none:
594+
return nil
595+
}
579596
}
580597

581598
// MARK: Migration

Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP2Connections.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,22 @@ extension HTTPConnectionPool {
187187
}
188188
}
189189

190-
mutating func fail() {
190+
enum FailAction {
191+
case removeConnection
192+
case none
193+
}
194+
195+
mutating func fail() -> FailAction {
191196
switch self.state {
192-
case .starting, .active, .backingOff, .draining:
197+
case .starting:
198+
// If the connection fails while we are starting it, the fail call raced with
199+
// `failedToConnect` (promises are succeeded or failed before channel handler
200+
// callbacks). let's keep the state in `starting`, so that `failedToConnect` can
201+
// create a backoff timer.
202+
return .none
203+
case .active, .backingOff, .draining:
193204
self.state = .closed
205+
return .removeConnection
194206
case .closed:
195207
preconditionFailure("Invalid state: \(self.state)")
196208
}
@@ -749,10 +761,16 @@ extension HTTPConnectionPool {
749761
// must ignore the event.
750762
return nil
751763
}
752-
self.connections[index].fail()
753-
let eventLoop = self.connections[index].eventLoop
754-
let context = FailedConnectionContext(eventLoop: eventLoop)
755-
return (index, context)
764+
765+
switch self.connections[index].fail() {
766+
case .none:
767+
return nil
768+
769+
case .removeConnection:
770+
let eventLoop = self.connections[index].eventLoop
771+
let context = FailedConnectionContext(eventLoop: eventLoop)
772+
return (index, context)
773+
}
756774
}
757775

758776
mutating func shutdown() -> CleanupContext {

Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP1StateTests.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,9 +1519,19 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
15191519
}
15201520
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
15211521

1522-
// 2. connection fails
1522+
// 2. connection fails – first with closed callback
1523+
1524+
XCTAssertEqual(state.http1ConnectionClosed(connectionID), .none)
1525+
1526+
// 3. connection fails – with make connection callback
1527+
1528+
let action = state.failedToCreateNewConnection(IOError(errnoCode: -1, reason: "Test failure"), connectionID: connectionID)
1529+
XCTAssertEqual(action.request, .none)
1530+
guard case .scheduleBackoffTimer(connectionID, _, on: let backoffTimerEL) = action.connection else {
1531+
XCTFail("Unexpected connection action: \(action.connection)")
1532+
return
1533+
}
1534+
XCTAssertIdentical(connectionEL, backoffTimerEL)
15231535

1524-
state.http1ConnectionClosed(connectionID)
1525-
state.failedToCreateNewConnection(IOError(errnoCode: -1, reason: "Test failure"), connectionID: connectionID)
15261536
}
15271537
}

Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,47 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
15271527

15281528
XCTAssertEqual(state.http2ConnectionClosed(connection.id), .none)
15291529
}
1530+
1531+
func testFailConnectionRacesAgainstConnectionCreationFailed() {
1532+
let elg = EmbeddedEventLoopGroup(loops: 4)
1533+
defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) }
1534+
1535+
var state = HTTPConnectionPool.StateMachine(
1536+
idGenerator: .init(),
1537+
maximumConcurrentHTTP1Connections: 2,
1538+
retryConnectionEstablishment: true,
1539+
preferHTTP1: false,
1540+
maximumConnectionUses: nil,
1541+
preWarmedHTTP1ConnectionCount: 0
1542+
)
1543+
1544+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
1545+
let request = HTTPConnectionPool.Request(mockRequest)
1546+
1547+
let executeAction = state.executeRequest(request)
1548+
XCTAssertEqual(.scheduleRequestTimeout(for: request, on: mockRequest.eventLoop), executeAction.request)
1549+
1550+
// 1. connection attempt
1551+
guard case .createConnection(let connectionID, on: let connectionEL) = executeAction.connection else {
1552+
return XCTFail("Unexpected connection action: \(executeAction.connection)")
1553+
}
1554+
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
1555+
1556+
// 2. connection fails – first with closed callback
1557+
1558+
XCTAssertEqual(state.http2ConnectionClosed(connectionID), .none)
1559+
1560+
// 3. connection fails – with make connection callback
1561+
1562+
let action = state.failedToCreateNewConnection(IOError(errnoCode: -1, reason: "Test failure"), connectionID: connectionID)
1563+
XCTAssertEqual(action.request, .none)
1564+
guard case .scheduleBackoffTimer(connectionID, _, on: let backoffTimerEL) = action.connection else {
1565+
XCTFail("Unexpected connection action: \(action.connection)")
1566+
return
1567+
}
1568+
XCTAssertIdentical(connectionEL, backoffTimerEL)
1569+
}
1570+
15301571
}
15311572

15321573
/// Should be used if you have a value of statically unknown type and want to compare its value to another `Equatable` value.

0 commit comments

Comments
 (0)