@@ -33,19 +33,23 @@ extension HTTPConnectionPool {
33
33
/// The property was introduced to fail fast during testing.
34
34
/// Otherwise this should always be true and not turned off.
35
35
private let retryConnectionEstablishment : Bool
36
+ private let preWarmedConnectionCount : Int
36
37
37
38
init (
38
39
idGenerator: Connection . ID . Generator ,
39
40
maximumConcurrentConnections: Int ,
40
41
retryConnectionEstablishment: Bool ,
41
42
maximumConnectionUses: Int ? ,
43
+ preWarmedHTTP1ConnectionCount: Int ,
42
44
lifecycleState: StateMachine . LifecycleState
43
45
) {
44
46
self . connections = HTTP1Connections (
45
47
maximumConcurrentConnections: maximumConcurrentConnections,
46
48
generator: idGenerator,
47
- maximumConnectionUses: maximumConnectionUses
49
+ maximumConnectionUses: maximumConnectionUses,
50
+ preWarmedHTTP1ConnectionCount: preWarmedHTTP1ConnectionCount
48
51
)
52
+ self . preWarmedConnectionCount = preWarmedHTTP1ConnectionCount
49
53
self . retryConnectionEstablishment = retryConnectionEstablishment
50
54
51
55
self . requests = RequestQueue ( )
@@ -145,9 +149,26 @@ extension HTTPConnectionPool {
145
149
146
150
private mutating func executeRequestOnPreferredEventLoop( _ request: Request , eventLoop: EventLoop ) -> Action {
147
151
if let connection = self . connections. leaseConnection ( onPreferred: eventLoop) {
152
+ // Cool, a connection is available. If using this would put us below our needed extra set, we
153
+ // should create another.
154
+ let stats = self . connections. generalPurposeStats
155
+ let needExtraConnection =
156
+ stats. nonLeased < ( self . requests. count + self . preWarmedConnectionCount) && self . connections. canGrow
157
+ let action : StateMachine . ConnectionAction
158
+
159
+ if needExtraConnection {
160
+ action = . createConnectionAndCancelTimeoutTimer(
161
+ createdID: self . connections. createNewConnection ( on: eventLoop) ,
162
+ on: eventLoop,
163
+ cancelTimerID: connection. id
164
+ )
165
+ } else {
166
+ action = . cancelTimeoutTimer( connection. id)
167
+ }
168
+
148
169
return . init(
149
170
request: . executeRequest( request, connection, cancelTimeout: false ) ,
150
- connection: . cancelTimeoutTimer ( connection . id )
171
+ connection: action
151
172
)
152
173
}
153
174
@@ -294,7 +315,20 @@ extension HTTPConnectionPool {
294
315
}
295
316
}
296
317
297
- mutating func connectionIdleTimeout( _ connectionID: Connection . ID ) -> Action {
318
+ mutating func connectionIdleTimeout( _ connectionID: Connection . ID , on eventLoop: any EventLoop ) -> Action {
319
+ // Don't close idle connections if we need pre-warmed connections. Instead, re-arm the idle timer.
320
+ // We still want the idle timers to make sure we eventually fall below the pre-warmed limit.
321
+ if self . preWarmedConnectionCount > 0 {
322
+ let stats = self . connections. generalPurposeStats
323
+ if stats. idle <= self . preWarmedConnectionCount {
324
+ return . init(
325
+ request: . none,
326
+ connection: . scheduleTimeoutTimer( connectionID, on: eventLoop)
327
+ )
328
+ }
329
+ }
330
+
331
+ // Ok, we do actually want the connection count to go down.
298
332
guard let connection = self . connections. closeConnectionIfIdle ( connectionID) else {
299
333
// because of a race this connection (connection close runs against trigger of timeout)
300
334
// was already removed from the state machine.
@@ -410,11 +444,7 @@ extension HTTPConnectionPool {
410
444
case . running:
411
445
// Close the connection if it's expired.
412
446
if context. shouldBeClosed {
413
- let connection = self . connections. closeConnection ( at: index)
414
- return . init(
415
- request: . none,
416
- connection: . closeConnection( connection, isShutdown: . no)
417
- )
447
+ return self . nextActionForToBeClosedIdleConnection ( at: index, context: context)
418
448
} else {
419
449
switch context. use {
420
450
case . generalPurpose:
@@ -446,28 +476,63 @@ extension HTTPConnectionPool {
446
476
at index: Int ,
447
477
context: HTTP1Connections . IdleConnectionContext
448
478
) -> EstablishedAction {
479
+ var requestAction = HTTPConnectionPool . StateMachine. RequestAction. none
480
+ var parkedConnectionDetails : ( HTTPConnectionPool . Connection . ID , any EventLoop ) ? = nil
481
+
449
482
// 1. Check if there are waiting requests in the general purpose queue
450
483
if let request = self . requests. popFirst ( for: nil ) {
451
- return . init(
452
- request: . executeRequest( request, self . connections. leaseConnection ( at: index) , cancelTimeout: true ) ,
453
- connection: . none
484
+ requestAction = . executeRequest(
485
+ request,
486
+ self . connections. leaseConnection ( at: index) ,
487
+ cancelTimeout: true
454
488
)
455
489
}
456
490
457
491
// 2. Check if there are waiting requests in the matching eventLoop queue
458
- if let request = self . requests. popFirst ( for: context. eventLoop) {
459
- return . init(
460
- request: . executeRequest( request, self . connections. leaseConnection ( at: index) , cancelTimeout: true ) ,
461
- connection: . none
492
+ if case . none = requestAction, let request = self . requests. popFirst ( for: context. eventLoop) {
493
+ requestAction = . executeRequest(
494
+ request,
495
+ self . connections. leaseConnection ( at: index) ,
496
+ cancelTimeout: true
462
497
)
463
498
}
464
499
465
500
// 3. Create a timeout timer to ensure the connection is closed if it is idle for too
466
- // long.
467
- let ( connectionID, eventLoop) = self . connections. parkConnection ( at: index)
501
+ // long, assuming we don't already have a use for it.
502
+ if case . none = requestAction {
503
+ parkedConnectionDetails = self . connections. parkConnection ( at: index)
504
+ }
505
+
506
+ // 4. We may need to create another connection to make sure we have enough pre-warmed ones.
507
+ // We need to do that if we have fewer non-leased connections than we need pre-warmed ones _and_ the pool can grow.
508
+ // Note that in this case we don't need to account for the number of pending requests, as that is 0: step 1
509
+ // confirmed that.
510
+ let connectionAction : EstablishedConnectionAction
511
+
512
+ if self . connections. generalPurposeStats. nonLeased < self . preWarmedConnectionCount
513
+ && self . connections. canGrow
514
+ {
515
+ // Re-use the event loop of the connection that just got created.
516
+ if let parkedConnectionDetails {
517
+ let newConnectionID = self . connections. createNewConnection ( on: parkedConnectionDetails. 1 )
518
+ connectionAction = . scheduleTimeoutTimerAndCreateConnection(
519
+ timeoutID: parkedConnectionDetails. 0 ,
520
+ newConnectionID: newConnectionID,
521
+ on: parkedConnectionDetails. 1
522
+ )
523
+ } else {
524
+ let newConnectionID = self . connections. createNewConnection ( on: context. eventLoop)
525
+ connectionAction = . createConnection( connectionID: newConnectionID, on: context. eventLoop)
526
+ }
527
+ } else if let parkedConnectionDetails {
528
+ connectionAction = . scheduleTimeoutTimer( parkedConnectionDetails. 0 , on: parkedConnectionDetails. 1 )
529
+ } else {
530
+ connectionAction = . none
531
+ }
532
+
468
533
return . init(
469
- request: . none ,
470
- connection: . scheduleTimeoutTimer ( connectionID , on : eventLoop )
534
+ request: requestAction ,
535
+ connection: connectionAction
471
536
)
472
537
}
473
538
@@ -495,6 +560,37 @@ extension HTTPConnectionPool {
495
560
)
496
561
}
497
562
563
+ private mutating func nextActionForToBeClosedIdleConnection(
564
+ at index: Int ,
565
+ context: HTTP1Connections . IdleConnectionContext
566
+ ) -> EstablishedAction {
567
+ // Step 1: Tell the connection pool to drop what it knows about this object.
568
+ let connectionToClose = self . connections. closeConnection ( at: index)
569
+
570
+ // Step 2: Check whether we need a connection to replace this one. We do if we have fewer non-leased connections
571
+ // than we requests + minimumPrewarming count _and_ the pool can grow. Note that in many cases the above closure
572
+ // will have made some space, which is just fine.
573
+ let nonLeased = self . connections. generalPurposeStats. nonLeased
574
+ let neededNonLeased = self . requests. generalPurposeCount + self . preWarmedConnectionCount
575
+
576
+ let connectionAction : EstablishedConnectionAction
577
+ if nonLeased < neededNonLeased && self . connections. canGrow {
578
+ // We re-use the EL of the connection we just closed.
579
+ let newConnectionID = self . connections. createNewConnection ( on: connectionToClose. eventLoop)
580
+ connectionAction = . closeConnectionAndCreateConnection(
581
+ closeConnection: connectionToClose,
582
+ newConnectionID: newConnectionID,
583
+ on: connectionToClose. eventLoop
584
+ )
585
+ } else {
586
+ connectionAction = . closeConnection( connectionToClose, isShutdown: . no)
587
+ }
588
+ return . init(
589
+ request: . none,
590
+ connection: connectionAction
591
+ )
592
+ }
593
+
498
594
// MARK: Failed/Closed connection management
499
595
500
596
private mutating func nextActionForFailedConnection(
@@ -530,7 +626,10 @@ extension HTTPConnectionPool {
530
626
at index: Int ,
531
627
context: HTTP1Connections . FailedConnectionContext
532
628
) -> Action {
533
- if context. connectionsStartingForUseCase < self . requests. generalPurposeCount {
629
+ let needConnectionForRequest =
630
+ context. connectionsStartingForUseCase
631
+ < ( self . requests. generalPurposeCount + self . preWarmedConnectionCount)
632
+ if needConnectionForRequest {
534
633
// if we have more requests queued up, than we have starting connections, we should
535
634
// create a new connection
536
635
let ( newConnectionID, newEventLoop) = self . connections. replaceConnection ( at: index)
0 commit comments