diff --git a/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP2StateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP2StateMachine.swift index 003de4223..9964ccd05 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP2StateMachine.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP2StateMachine.swift @@ -404,25 +404,34 @@ extension HTTPConnectionPool { } mutating func failedToCreateNewConnection(_ error: Error, connectionID: Connection.ID) -> Action { - // TODO: switch over state https://github.com/swift-server/async-http-client/issues/638 self.failedConsecutiveConnectionAttempts += 1 self.lastConnectFailure = error - guard self.retryConnectionEstablishment else { - guard let (index, _) = self.connections.failConnection(connectionID) else { - preconditionFailure("A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost.") + switch self.lifecycleState { + case .running: + guard self.retryConnectionEstablishment else { + guard let (index, _) = self.connections.failConnection(connectionID) else { + preconditionFailure("A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost.") + } + self.connections.removeConnection(at: index) + + return .init( + request: self.failAllRequests(reason: error), + connection: .none + ) } - self.connections.removeConnection(at: index) - return .init( - request: self.failAllRequests(reason: error), - connection: .none - ) + let eventLoop = self.connections.backoffNextConnectionAttempt(connectionID) + let backoff = calculateBackoff(failedAttempt: self.failedConsecutiveConnectionAttempts) + return .init(request: .none, connection: .scheduleBackoffTimer(connectionID, backoff: backoff, on: eventLoop)) + case .shuttingDown: + guard let (index, context) = self.connections.failConnection(connectionID) else { + preconditionFailure("A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost.") + } + return self.nextActionForFailedConnection(at: index, on: context.eventLoop) + case .shutDown: + preconditionFailure("If the pool is already shutdown, all connections must have been torn down.") } - - let eventLoop = self.connections.backoffNextConnectionAttempt(connectionID) - let backoff = calculateBackoff(failedAttempt: self.failedConsecutiveConnectionAttempts) - return .init(request: .none, connection: .scheduleBackoffTimer(connectionID, backoff: backoff, on: eventLoop)) } mutating func waitingForConnectivity(_ error: Error, connectionID: Connection.ID) -> Action { diff --git a/Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests+XCTest.swift index 95ea8c580..12b031cc0 100644 --- a/Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests+XCTest.swift @@ -27,6 +27,7 @@ extension HTTPConnectionPool_HTTP2StateMachineTests { return [ ("testCreatingOfConnection", testCreatingOfConnection), ("testConnectionFailureBackoff", testConnectionFailureBackoff), + ("testConnectionFailureWhileShuttingDown", testConnectionFailureWhileShuttingDown), ("testConnectionFailureWithoutRetry", testConnectionFailureWithoutRetry), ("testCancelRequestWorks", testCancelRequestWorks), ("testExecuteOnShuttingDownPool", testExecuteOnShuttingDownPool), diff --git a/Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests.swift b/Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests.swift index bf1ef4a98..10fad7bd6 100644 --- a/Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP2StateMachineTests.swift @@ -194,6 +194,44 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase { XCTAssertEqual(state.connectionCreationBackoffDone(newConnectionID), .none) } + func testConnectionFailureWhileShuttingDown() { + struct SomeError: Error, Equatable {} + let elg = EmbeddedEventLoopGroup(loops: 4) + defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) } + + var state = HTTPConnectionPool.HTTP2StateMachine( + idGenerator: .init(), + retryConnectionEstablishment: false, + lifecycleState: .running + ) + + let mockRequest = MockHTTPRequest(eventLoop: elg.next()) + let request = HTTPConnectionPool.Request(mockRequest) + + let action = state.executeRequest(request) + XCTAssertEqual(.scheduleRequestTimeout(for: request, on: mockRequest.eventLoop), action.request) + + // 1. connection attempt + guard case .createConnection(let connectionID, on: let connectionEL) = action.connection else { + return XCTFail("Unexpected connection action: \(action.connection)") + } + XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux + + // 2. initialise shutdown + let shutdownAction = state.shutdown() + XCTAssertEqual(shutdownAction.connection, .cleanupConnections(.init(), isShutdown: .no)) + guard case .failRequestsAndCancelTimeouts(let requestsToFail, let requestError) = shutdownAction.request else { + return XCTFail("Unexpected request action: \(action.request)") + } + XCTAssertEqualTypeAndValue(requestError, HTTPClientError.cancelled) + XCTAssertEqualTypeAndValue(requestsToFail, [request]) + + // 3. connection attempt fails + let failedConnectAction = state.failedToCreateNewConnection(SomeError(), connectionID: connectionID) + XCTAssertEqual(failedConnectAction.request, .none) + XCTAssertEqual(failedConnectAction.connection, .cleanupConnections(.init(), isShutdown: .yes(unclean: true))) + } + func testConnectionFailureWithoutRetry() { struct SomeError: Error, Equatable {} let elg = EmbeddedEventLoopGroup(loops: 4)