Skip to content

Commit d578b86

Browse files
authored
Fix crash where backoff timer triggers during running state (#617)
1 parent 46b2b3d commit d578b86

File tree

2 files changed

+55
-1
lines changed

2 files changed

+55
-1
lines changed

Sources/ConnectionPoolModule/PoolStateMachine.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ struct PoolStateMachine<
524524
}
525525

526526
case .running:
527-
preconditionFailure("Invalid state")
527+
break
528528

529529
case .shuttingDown, .shutDown:
530530
return .none()

Tests/ConnectionPoolModuleTests/PoolStateMachineTests.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,60 @@ typealias TestPoolStateMachine = PoolStateMachine<
648648
#expect(stateMachine.connections.stats.active == 1)
649649
}
650650

651+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
652+
@Test func testTwoConnectionsFailAndSucceed() throws {
653+
struct ConnectionFailed: Error, Equatable {}
654+
let clock = MockClock()
655+
var configuration = PoolConfiguration()
656+
configuration.minimumConnectionCount = 0
657+
configuration.maximumConnectionSoftLimit = 10
658+
configuration.maximumConnectionHardLimit = 10
659+
configuration.keepAliveDuration = .seconds(2)
660+
configuration.idleTimeoutDuration = .seconds(4)
661+
configuration.maximumConcurrentConnectionRequests = 3
662+
663+
var stateMachine = TestPoolStateMachine(
664+
configuration: configuration,
665+
generator: .init(),
666+
timerCancellationTokenType: MockTimerCancellationToken.self,
667+
clock: clock
668+
)
669+
670+
// request two connections
671+
let mockRequest1 = MockRequest(connectionType: MockConnection.self)
672+
let leaseAction1 = stateMachine.leaseConnection(mockRequest1)
673+
guard case .makeConnection(let request1, _) = leaseAction1.connection else {
674+
Issue.record()
675+
return
676+
}
677+
let mockRequest2 = MockRequest(connectionType: MockConnection.self)
678+
let leaseAction2 = stateMachine.leaseConnection(mockRequest2)
679+
guard case .makeConnection = leaseAction2.connection else {
680+
Issue.record()
681+
return
682+
}
683+
684+
// fail connection 1
685+
let failedAction = stateMachine.connectionEstablishFailed(ConnectionFailed(), for: request1)
686+
#expect(failedAction.request == .none)
687+
guard case .scheduleTimers(let timers) = failedAction.connection else {
688+
Issue.record()
689+
return
690+
}
691+
#expect(timers.count == 1)
692+
let timer = try #require(timers.first)
693+
#expect(timer.underlying.usecase == .backoff)
694+
695+
// make connection
696+
let connection = MockConnection(id: 1)
697+
let createdAction = stateMachine.connectionEstablished(connection, maxStreams: 1)
698+
#expect(createdAction.request == .leaseConnection([mockRequest1], connection))
699+
700+
// backoff timer from failed connection triggers
701+
let timerAction = stateMachine.timerTriggered(timer)
702+
#expect(timerAction.connection == .makeConnection(request1, []))
703+
}
704+
651705
/// Test that we limit concurrent connection requests and that when connections are established
652706
/// we request new connections
653707
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)

0 commit comments

Comments
 (0)