Skip to content

Commit f047858

Browse files
authored
Fix indexing error in triggerForceShutdown (#608)
1 parent e017bd9 commit f047858

File tree

4 files changed

+72
-4
lines changed

4 files changed

+72
-4
lines changed

Sources/ConnectionPoolModule/PoolStateMachine+ConnectionGroup.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ extension PoolStateMachine {
525525
}
526526
/// Closes the connection at the given index.
527527
@inlinable
528-
mutating func closeConnection(at index: Int) -> CloseConnectionAction {
528+
mutating func closeConnection(at index: Int, deleteConnection: Bool) -> CloseConnectionAction {
529529
guard let closeAction = self.connections[index].close() else {
530530
return .doNothing // no action to take
531531
}
@@ -557,7 +557,7 @@ extension PoolStateMachine {
557557
} else {
558558
// if there is no connection we should delete this now
559559
var timersToCancel = closeAction.cancelTimers
560-
if let cancellationTimer = self.swapForDeletion(index: index) {
560+
if deleteConnection, let cancellationTimer = self.swapForDeletion(index: index) {
561561
timersToCancel.append(cancellationTimer)
562562
}
563563
return .cancelTimers(timersToCancel)
@@ -674,7 +674,7 @@ extension PoolStateMachine {
674674

675675
mutating func triggerForceShutdown(_ cleanup: inout ConnectionAction.Shutdown) {
676676
for index in self.connections.indices {
677-
switch closeConnection(at: index) {
677+
switch closeConnection(at: index, deleteConnection: false) {
678678
case .close(let closeAction):
679679
cleanup.connections.append(closeAction.connection)
680680
cleanup.timersToCancel.append(contentsOf: closeAction.timersToCancel)
@@ -686,6 +686,7 @@ extension PoolStateMachine {
686686
break
687687
}
688688
}
689+
self.connections = self.connections.filter { !$0.isClosed }
689690
}
690691

691692
// MARK: - Private functions -

Sources/ConnectionPoolModule/PoolStateMachine+ConnectionState.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ extension PoolStateMachine {
184184
}
185185
}
186186

187+
@inlinable
188+
var isClosed: Bool {
189+
switch self.state {
190+
case .closed:
191+
return true
192+
case .idle, .leased, .backingOff, .starting, .closing:
193+
return false
194+
}
195+
}
196+
187197
@inlinable
188198
mutating func connected(_ connection: Connection, maxStreams: UInt16) -> ConnectionAvailableInfo {
189199
switch self.state {

Sources/ConnectionPoolModule/PoolStateMachine.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ struct PoolStateMachine<
526526

527527
case .idle(_, let newIdle):
528528
if case .shuttingDown = self.poolState {
529-
switch self.connections.closeConnection(at: index) {
529+
switch self.connections.closeConnection(at: index, deleteConnection: true) {
530530
case .close(let closeAction):
531531
return .init(
532532
request: .none,

Tests/ConnectionPoolModuleTests/PoolStateMachineTests.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,4 +507,61 @@ typealias TestPoolStateMachine = PoolStateMachine<
507507

508508
#expect(stateMachine.poolState == .shutDown)
509509
}
510+
511+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
512+
@Test func testTriggerForceShutdownWithBackingOffRequest() {
513+
struct ConnectionFailed: Error {}
514+
var configuration = PoolConfiguration()
515+
configuration.minimumConnectionCount = 2
516+
configuration.maximumConnectionSoftLimit = 2
517+
configuration.maximumConnectionHardLimit = 2
518+
configuration.keepAliveDuration = .seconds(2)
519+
configuration.idleTimeoutDuration = .seconds(4)
520+
521+
var stateMachine = TestPoolStateMachine(
522+
configuration: configuration,
523+
generator: .init(),
524+
timerCancellationTokenType: MockTimerCancellationToken.self
525+
)
526+
527+
// refill pool
528+
let requests = stateMachine.refillConnections()
529+
#expect(requests.count == 2)
530+
531+
// Add two connections to verify we don't use an out of bounds index when iterating the
532+
// connection array on triggerForceShutdown. The first connection will be deleted as it
533+
// never connected. Need to be sure when we access the second connection it is with the
534+
// correct index
535+
536+
// fail connection 1
537+
let failedAction = stateMachine.connectionEstablishFailed(ConnectionFailed(), for: requests[0])
538+
print(failedAction)
539+
#expect(failedAction.request == .none)
540+
switch failedAction.connection {
541+
case .scheduleTimers(let timers):
542+
#expect(timers.count == 1)
543+
#expect(timers.first?.underlying.usecase == .backoff)
544+
default:
545+
Issue.record()
546+
}
547+
548+
// make connection 2
549+
let connection2 = MockConnection(id: 1)
550+
let createdAction = stateMachine.connectionEstablished(connection2, maxStreams: 1)
551+
print(createdAction)
552+
let connection2KeepAliveTimer = TestPoolStateMachine.Timer(.init(timerID: 0, connectionID: 1, usecase: .keepAlive), duration: .seconds(2))
553+
#expect(createdAction.request == .none)
554+
#expect(createdAction.connection == .scheduleTimers([connection2KeepAliveTimer]))
555+
556+
let shutdownAction = stateMachine.triggerForceShutdown()
557+
print(shutdownAction)
558+
var shutdown = TestPoolStateMachine.ConnectionAction.Shutdown()
559+
shutdown.connections = [connection2]
560+
#expect(shutdownAction.connection == .initiateShutdown(shutdown))
561+
562+
let closedAction = stateMachine.connectionClosed(connection2)
563+
#expect(closedAction.connection == .cancelEventStreamAndFinalCleanup([]))
564+
565+
#expect(stateMachine.poolState == .shutDown)
566+
}
510567
}

0 commit comments

Comments
 (0)