@@ -24,15 +24,17 @@ protocol HTTPConnectionPoolDelegate {
24
24
final class HTTPConnectionPool {
25
25
private let stateLock = Lock ( )
26
26
private var _state : StateMachine
27
+ /// The connection idle timeout timers. Protected by the stateLock
28
+ private var _idleTimer = [ Connection . ID : Scheduled < Void > ] ( )
29
+ /// The connection backoff timeout timers. Protected by the stateLock
30
+ private var _backoffTimer = [ Connection . ID : Scheduled < Void > ] ( )
27
31
28
32
private static let fallbackConnectTimeout : TimeAmount = . seconds( 30 )
29
33
30
34
let key : ConnectionPool . Key
31
35
32
36
private let timerLock = Lock ( )
33
37
private var _requestTimer = [ Request . ID : Scheduled < Void > ] ( )
34
- private var _idleTimer = [ Connection . ID : Scheduled < Void > ] ( )
35
- private var _backoffTimer = [ Connection . ID : Scheduled < Void > ] ( )
36
38
37
39
private var logger : Logger
38
40
@@ -75,32 +77,91 @@ final class HTTPConnectionPool {
75
77
}
76
78
77
79
func executeRequest( _ request: HTTPSchedulableRequest ) {
78
- let action = self . stateLock. withLock { ( ) -> StateMachine . Action in
79
- self . _state. executeRequest ( . init( request) )
80
- }
81
- self . run ( action: action)
80
+ self . modifyStateAndRunActions { $0. executeRequest ( . init( request) ) }
82
81
}
83
82
84
83
func shutdown( ) {
85
- let action = self . stateLock. withLock { ( ) -> StateMachine . Action in
86
- self . _state. shutdown ( )
84
+ self . modifyStateAndRunActions { $0. shutdown ( ) }
85
+ }
86
+
87
+ // MARK: - Private Methods -
88
+
89
+ // MARK: Actions
90
+
91
+ /// An `HTTPConnectionPool` internal action type that matches the `StateMachine`'s action.
92
+ /// However it splits up the actions into actions that need to be executed inside the `stateLock`
93
+ /// and outside the `stateLock`.
94
+ private struct Actions {
95
+ enum ConnectionAction {
96
+ enum Unlocked {
97
+ case createConnection( Connection . ID , on: EventLoop )
98
+ case closeConnection( Connection , isShutdown: StateMachine . ConnectionAction . IsShutdown )
99
+ case cleanupConnections( CleanupContext , isShutdown: StateMachine . ConnectionAction . IsShutdown )
100
+ case none
101
+ }
102
+
103
+ enum Locked {
104
+ case scheduleBackoffTimer( Connection . ID , backoff: TimeAmount , on: EventLoop )
105
+ case cancelBackoffTimers( [ Connection . ID ] )
106
+ case scheduleTimeoutTimer( Connection . ID , on: EventLoop )
107
+ case cancelTimeoutTimer( Connection . ID )
108
+ case none
109
+ }
110
+ }
111
+
112
+ struct Locked {
113
+ var connection : ConnectionAction . Locked
114
+ }
115
+
116
+ struct Unlocked {
117
+ var connection : ConnectionAction . Unlocked
118
+ var request : StateMachine . RequestAction
119
+ }
120
+
121
+ var locked : Locked
122
+ var unlocked : Unlocked
123
+
124
+ init ( from stateMachineAction: StateMachine . Action ) {
125
+ self . locked = Locked ( connection: . none)
126
+ self . unlocked = Unlocked ( connection: . none, request: stateMachineAction. request)
127
+
128
+ switch stateMachineAction. connection {
129
+ case . createConnection( let connectionID, on: let eventLoop) :
130
+ self . unlocked. connection = . createConnection( connectionID, on: eventLoop)
131
+ case . scheduleBackoffTimer( let connectionID, backoff: let backoff, on: let eventLoop) :
132
+ self . locked. connection = . scheduleBackoffTimer( connectionID, backoff: backoff, on: eventLoop)
133
+ case . scheduleTimeoutTimer( let connectionID, on: let eventLoop) :
134
+ self . locked. connection = . scheduleTimeoutTimer( connectionID, on: eventLoop)
135
+ case . cancelTimeoutTimer( let connectionID) :
136
+ self . locked. connection = . cancelTimeoutTimer( connectionID)
137
+ case . closeConnection( let connection, isShutdown: let isShutdown) :
138
+ self . unlocked. connection = . closeConnection( connection, isShutdown: isShutdown)
139
+ case . cleanupConnections( var cleanupContext, isShutdown: let isShutdown) :
140
+ //
141
+ self . locked. connection = . cancelBackoffTimers( cleanupContext. connectBackoff)
142
+ cleanupContext. connectBackoff = [ ]
143
+ self . unlocked. connection = . cleanupConnections( cleanupContext, isShutdown: isShutdown)
144
+ case . none:
145
+ break
146
+ }
87
147
}
88
- self . run ( action: action)
89
148
}
90
149
91
150
// MARK: Run actions
92
151
93
- private func run( action: StateMachine . Action ) {
94
- self . runConnectionAction ( action. connection)
95
- self . runRequestAction ( action. request)
152
+ private func modifyStateAndRunActions( _ closure: ( inout StateMachine ) -> StateMachine . Action ) {
153
+ let unlockedActions = self . stateLock. withLock { ( ) -> Actions . Unlocked in
154
+ let stateMachineAction = closure ( & self . _state)
155
+ let poolAction = Actions ( from: stateMachineAction)
156
+ self . runLockedActions ( poolAction. locked)
157
+ return poolAction. unlocked
158
+ }
159
+ self . runUnlockedActions ( unlockedActions)
96
160
}
97
161
98
- private func runConnectionAction( _ action: StateMachine . ConnectionAction ) {
99
- switch action {
100
- case . createConnection( let connectionID, let eventLoop) :
101
- self . createConnection ( connectionID, on: eventLoop)
102
-
103
- case . scheduleBackoffTimer( let connectionID, let backoff, on: let eventLoop) :
162
+ private func runLockedActions( _ actions: Actions . Locked ) {
163
+ switch actions. connection {
164
+ case . scheduleBackoffTimer( let connectionID, backoff: let backoff, on: let eventLoop) :
104
165
self . scheduleConnectionStartBackoffTimer ( connectionID, backoff, on: eventLoop)
105
166
106
167
case . scheduleTimeoutTimer( let connectionID, on: let eventLoop) :
@@ -109,6 +170,26 @@ final class HTTPConnectionPool {
109
170
case . cancelTimeoutTimer( let connectionID) :
110
171
self . cancelIdleTimerForConnection ( connectionID)
111
172
173
+ case . cancelBackoffTimers( let connectionIDs) :
174
+ for connectionID in connectionIDs {
175
+ self . cancelConnectionStartBackoffTimer ( connectionID)
176
+ }
177
+
178
+ case . none:
179
+ break
180
+ }
181
+ }
182
+
183
+ private func runUnlockedActions( _ actions: Actions . Unlocked ) {
184
+ self . runUnlockedConnectionAction ( actions. connection)
185
+ self . runUnlockedRequestAction ( actions. request)
186
+ }
187
+
188
+ private func runUnlockedConnectionAction( _ action: Actions . ConnectionAction . Unlocked ) {
189
+ switch action {
190
+ case . createConnection( let connectionID, let eventLoop) :
191
+ self . createConnection ( connectionID, on: eventLoop)
192
+
112
193
case . closeConnection( let connection, isShutdown: let isShutdown) :
113
194
self . logger. trace ( " close connection " , metadata: [
114
195
" ahc-connection-id " : " \( connection. id) " ,
@@ -143,7 +224,7 @@ final class HTTPConnectionPool {
143
224
}
144
225
}
145
226
146
- private func runRequestAction ( _ action: StateMachine . RequestAction ) {
227
+ private func runUnlockedRequestAction ( _ action: StateMachine . RequestAction ) {
147
228
// The order of execution fail/execute request vs cancelling the request timeout timer does
148
229
// not matter in the actions here. The actions don't cause any side effects that will be
149
230
// reported back to the state machine and are not dependent on each other.
@@ -215,11 +296,9 @@ final class HTTPConnectionPool {
215
296
guard timeoutFired else { return }
216
297
217
298
// 3. Tell the state machine about the timeout
218
- let action = self . stateLock . withLock {
219
- self . _state . timeoutRequest ( requestID)
299
+ self . modifyStateAndRunActions {
300
+ $0 . timeoutRequest ( requestID)
220
301
}
221
-
222
- self . run ( action: action)
223
302
}
224
303
225
304
self . timerLock. withLockVoid {
@@ -254,34 +333,27 @@ final class HTTPConnectionPool {
254
333
let scheduled = eventLoop. scheduleTask ( in: self . idleConnectionTimeout) {
255
334
// there might be a race between a cancelTimer call and the triggering
256
335
// of this scheduled task. both want to acquire the lock
257
- let timerExisted = self . timerLock. withLock {
258
- self . _idleTimer. removeValue ( forKey: connectionID) != nil
336
+ self . modifyStateAndRunActions { stateMachine in
337
+ if self . _idleTimer. removeValue ( forKey: connectionID) != nil {
338
+ // The timer still exists. State Machines assumes it is alive
339
+ return stateMachine. connectionIdleTimeout ( connectionID)
340
+ }
341
+ return . none
259
342
}
260
-
261
- guard timerExisted else { return }
262
-
263
- let action = self . stateLock. withLock {
264
- self . _state. connectionIdleTimeout ( connectionID)
265
- }
266
- self . run ( action: action)
267
343
}
268
344
269
- self . timerLock. withLock {
270
- assert ( self . _idleTimer [ connectionID] == nil )
271
- self . _idleTimer [ connectionID] = scheduled
272
- }
345
+ assert ( self . _idleTimer [ connectionID] == nil )
346
+ self . _idleTimer [ connectionID] = scheduled
273
347
}
274
348
275
349
private func cancelIdleTimerForConnection( _ connectionID: Connection . ID ) {
276
350
self . logger. trace ( " Cancel idle connection timeout timer " , metadata: [
277
351
" ahc-connection-id " : " \( connectionID) " ,
278
352
] )
279
-
280
- let cancelTimer = self . timerLock. withLock {
281
- self . _idleTimer. removeValue ( forKey: connectionID)
353
+ guard let cancelTimer = self . _idleTimer. removeValue ( forKey: connectionID) else {
354
+ preconditionFailure ( " Expected to have an idle timer for connection \( connectionID) at this point. " )
282
355
}
283
-
284
- cancelTimer? . cancel ( )
356
+ cancelTimer. cancel ( )
285
357
}
286
358
287
359
private func scheduleConnectionStartBackoffTimer(
@@ -295,30 +367,24 @@ final class HTTPConnectionPool {
295
367
296
368
let scheduled = eventLoop. scheduleTask ( in: timeAmount) {
297
369
// there might be a race between a backoffTimer and the pool shutting down.
298
- let timerExisted = self . timerLock. withLock {
299
- self . _backoffTimer. removeValue ( forKey: connectionID) != nil
300
- }
301
-
302
- guard timerExisted else { return }
303
-
304
- let action = self . stateLock. withLock {
305
- self . _state. connectionCreationBackoffDone ( connectionID)
370
+ self . modifyStateAndRunActions { stateMachine in
371
+ if self . _backoffTimer. removeValue ( forKey: connectionID) != nil {
372
+ // The timer still exists. State Machines assumes it is alive
373
+ return stateMachine. connectionCreationBackoffDone ( connectionID)
374
+ }
375
+ return . none
306
376
}
307
- self . run ( action: action)
308
377
}
309
378
310
- self . timerLock. withLock {
311
- assert ( self . _backoffTimer [ connectionID] == nil )
312
- self . _backoffTimer [ connectionID] = scheduled
313
- }
379
+ assert ( self . _backoffTimer [ connectionID] == nil )
380
+ self . _backoffTimer [ connectionID] = scheduled
314
381
}
315
382
316
383
private func cancelConnectionStartBackoffTimer( _ connectionID: Connection . ID ) {
317
- let backoffTimer = self . timerLock . withLock {
318
- self . _backoffTimer [ connectionID]
384
+ guard let backoffTimer = self . _backoffTimer . removeValue ( forKey : connectionID ) else {
385
+ preconditionFailure ( " Expected to have a backoff timer for connection \( connectionID) at this point. " )
319
386
}
320
-
321
- backoffTimer? . cancel ( )
387
+ backoffTimer. cancel ( )
322
388
}
323
389
}
324
390
@@ -330,10 +396,9 @@ extension HTTPConnectionPool: HTTPConnectionRequester {
330
396
" ahc-connection-id " : " \( connection. id) " ,
331
397
" ahc-http-version " : " http/1.1 " ,
332
398
] )
333
- let action = self . stateLock . withLock {
334
- self . _state . newHTTP1ConnectionCreated ( . http1_1( connection) )
399
+ self . modifyStateAndRunActions {
400
+ $0 . newHTTP1ConnectionCreated ( . http1_1( connection) )
335
401
}
336
- self . run ( action: action)
337
402
}
338
403
339
404
func http2ConnectionCreated( _ connection: HTTP2Connection , maximumStreams: Int ) {
@@ -356,10 +421,9 @@ extension HTTPConnectionPool: HTTPConnectionRequester {
356
421
" ahc-error " : " \( error) " ,
357
422
" ahc-connection-id " : " \( connectionID) " ,
358
423
] )
359
- let action = self . stateLock . withLock {
360
- self . _state . failedToCreateNewConnection ( error, connectionID: connectionID)
424
+ self . modifyStateAndRunActions {
425
+ $0 . failedToCreateNewConnection ( error, connectionID: connectionID)
361
426
}
362
- self . run ( action: action)
363
427
}
364
428
}
365
429
@@ -369,21 +433,19 @@ extension HTTPConnectionPool: HTTP1ConnectionDelegate {
369
433
" ahc-connection-id " : " \( connection. id) " ,
370
434
" ahc-http-version " : " http/1.1 " ,
371
435
] )
372
- let action = self . stateLock . withLock {
373
- self . _state . connectionClosed ( connection. id)
436
+ self . modifyStateAndRunActions {
437
+ $0 . connectionClosed ( connection. id)
374
438
}
375
- self . run ( action: action)
376
439
}
377
440
378
441
func http1ConnectionReleased( _ connection: HTTP1Connection ) {
379
442
self . logger. trace ( " releasing connection " , metadata: [
380
443
" ahc-connection-id " : " \( connection. id) " ,
381
444
" ahc-http-version " : " http/1.1 " ,
382
445
] )
383
- let action = self . stateLock . withLock {
384
- self . _state . http1ConnectionReleased ( connection. id)
446
+ self . modifyStateAndRunActions {
447
+ $0 . http1ConnectionReleased ( connection. id)
385
448
}
386
- self . run ( action: action)
387
449
}
388
450
}
389
451
@@ -416,10 +478,9 @@ extension HTTPConnectionPool: HTTP2ConnectionDelegate {
416
478
extension HTTPConnectionPool : HTTPRequestScheduler {
417
479
func cancelRequest( _ request: HTTPSchedulableRequest ) {
418
480
let requestID = Request ( request) . id
419
- let action = self . stateLock . withLock {
420
- self . _state . cancelRequest ( requestID)
481
+ self . modifyStateAndRunActions {
482
+ $0 . cancelRequest ( requestID)
421
483
}
422
- self . run ( action: action)
423
484
}
424
485
}
425
486
0 commit comments