Skip to content

Commit 6eda4da

Browse files
committed
Code review
1 parent 9fc3530 commit 6eda4da

7 files changed

+88
-65
lines changed

Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1Connections.swift

+5-4
Original file line numberDiff line numberDiff line change
@@ -451,12 +451,13 @@ extension HTTPConnectionPool {
451451
/// This will put the position into the closed state.
452452
///
453453
/// - Parameter connectionID: The failed connection's id.
454-
/// - Returns: An index and an IdleConnectionContext to determine the next action for the now closed connection.
454+
/// - Returns: An optional index and an IdleConnectionContext to determine the next action for the closed connection.
455455
/// You must call ``removeConnection(at:)`` or ``replaceConnection(at:)`` with the
456-
/// supplied index after this.
457-
mutating func failConnection(_ connectionID: Connection.ID) -> (Int, FailedConnectionContext) {
456+
/// supplied index after this. If nil is returned the connection was closed by the state machine and was
457+
/// therefore already removed.
458+
mutating func failConnection(_ connectionID: Connection.ID) -> (Int, FailedConnectionContext)? {
458459
guard let index = self.connections.firstIndex(where: { $0.connectionID == connectionID }) else {
459-
preconditionFailure("We tried to fail a new connection that we know nothing about?")
460+
return nil
460461
}
461462

462463
let use: ConnectionUse

Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1StateMachine.swift

+34-29
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension HTTPConnectionPool {
2727
private var connections: HTTP1Connections
2828
private var failedConsecutiveConnectionAttempts: Int = 0
2929

30-
private var queue: RequestQueue
30+
private var requests: RequestQueue
3131
private var state: State = .running
3232

3333
init(idGenerator: Connection.ID.Generator, maximumConcurrentConnections: Int) {
@@ -36,7 +36,7 @@ extension HTTPConnectionPool {
3636
generator: idGenerator
3737
)
3838

39-
self.queue = RequestQueue()
39+
self.requests = RequestQueue()
4040
}
4141

4242
// MARK: - Events -
@@ -72,7 +72,7 @@ extension HTTPConnectionPool {
7272
}
7373

7474
// No matter what we do now, the request will need to wait!
75-
self.queue.push(request)
75+
self.requests.push(request)
7676
let requestAction: StateMachine.RequestAction = .scheduleRequestTimeout(
7777
for: request,
7878
on: eventLoop
@@ -84,7 +84,7 @@ extension HTTPConnectionPool {
8484
}
8585

8686
// if we are not at max connections, we may want to create a new connection
87-
if self.connections.startingGeneralPurposeConnections >= self.queue.count {
87+
if self.connections.startingGeneralPurposeConnections >= self.requests.generalPurposeCount {
8888
// If there are at least as many connections starting as we have request queued, we
8989
// don't need to create a new connection. we just need to wait.
9090
return .init(request: requestAction, connection: .none)
@@ -109,14 +109,14 @@ extension HTTPConnectionPool {
109109
}
110110

111111
// No matter what we do now, the request will need to wait!
112-
self.queue.push(request)
112+
self.requests.push(request)
113113
let requestAction: StateMachine.RequestAction = .scheduleRequestTimeout(
114114
for: request,
115115
on: eventLoop
116116
)
117117

118118
let starting = self.connections.startingEventLoopConnections(on: eventLoop)
119-
let waiting = self.queue.count(for: eventLoop)
119+
let waiting = self.requests.count(for: eventLoop)
120120

121121
if starting >= waiting {
122122
// There are already as many connections starting as we need for the waiting
@@ -141,11 +141,7 @@ extension HTTPConnectionPool {
141141
}
142142

143143
mutating func failedToCreateNewConnection(_ error: Error, connectionID: Connection.ID) -> Action {
144-
defer {
145-
// After we have processed this call, we have one more failed connection attempt,
146-
// that we need to consider when computing the jitter.
147-
self.failedConsecutiveConnectionAttempts += 1
148-
}
144+
self.failedConsecutiveConnectionAttempts += 1
149145

150146
switch self.state {
151147
case .running:
@@ -154,14 +150,16 @@ extension HTTPConnectionPool {
154150
// decision about the retry will be made in `connectionCreationBackoffDone(_:)`
155151
let eventLoop = self.connections.backoffNextConnectionAttempt(connectionID)
156152

157-
let backoff = self.calculateBackoff(for: self.failedConsecutiveConnectionAttempts)
153+
let backoff = self.calculateBackoff(failedAttempt: self.failedConsecutiveConnectionAttempts)
158154
return .init(
159155
request: .none,
160156
connection: .scheduleBackoffTimer(connectionID, backoff: backoff, on: eventLoop)
161157
)
162158

163159
case .shuttingDown:
164-
let (index, context) = self.connections.failConnection(connectionID)
160+
guard let (index, context) = self.connections.failConnection(connectionID) else {
161+
preconditionFailure("Failed to create a connection that is unknown to us?")
162+
}
165163
return self.nextActionForFailedConnection(at: index, context: context)
166164

167165
case .shutDown:
@@ -174,7 +172,9 @@ extension HTTPConnectionPool {
174172
// The naming of `failConnection` is a little confusing here. All it does is moving the
175173
// connection state from `.backingOff` to `.closed` here. It also returns the
176174
// connection's index.
177-
let (index, context) = self.connections.failConnection(connectionID)
175+
guard let (index, context) = self.connections.failConnection(connectionID) else {
176+
preconditionFailure("Backing off a connection that is unknown to us?")
177+
}
178178
// In `nextActionForFailedConnection` a decision will be made whether the failed
179179
// connection should be replaced or removed.
180180
return self.nextActionForFailedConnection(at: index, context: context)
@@ -202,13 +202,18 @@ extension HTTPConnectionPool {
202202

203203
/// A connection has been unexpectedly closed
204204
mutating func connectionClosed(_ connectionID: Connection.ID) -> Action {
205-
let (index, context) = self.connections.failConnection(connectionID)
205+
guard let (index, context) = self.connections.failConnection(connectionID) else {
206+
// When a connection close is initiated by the connection pool, the connection will
207+
// still report its close to the state machine. In those cases we must ignore the
208+
// event.
209+
return .none
210+
}
206211
return self.nextActionForFailedConnection(at: index, context: context)
207212
}
208213

209214
mutating func timeoutRequest(_ requestID: Request.ID) -> Action {
210215
// 1. check requests in queue
211-
if let request = self.queue.remove(requestID) {
216+
if let request = self.requests.remove(requestID) {
212217
return .init(
213218
request: .failRequest(request, HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout: false),
214219
connection: .none
@@ -223,7 +228,7 @@ extension HTTPConnectionPool {
223228

224229
mutating func cancelRequest(_ requestID: Request.ID) -> Action {
225230
// 1. check requests in queue
226-
if self.queue.remove(requestID) != nil {
231+
if self.requests.remove(requestID) != nil {
227232
return .init(
228233
request: .cancelRequestTimeout(requestID),
229234
connection: .none
@@ -241,7 +246,7 @@ extension HTTPConnectionPool {
241246

242247
// If we have remaining request queued, we should fail all of them with a cancelled
243248
// error.
244-
let waitingRequests = self.queue.removeAll()
249+
let waitingRequests = self.requests.removeAll()
245250

246251
var requestAction: StateMachine.RequestAction = .none
247252
if !waitingRequests.isEmpty {
@@ -285,7 +290,7 @@ extension HTTPConnectionPool {
285290
return self.nextActionForIdleEventLoopConnection(at: index, context: context)
286291
}
287292
case .shuttingDown(let unclean):
288-
assert(self.queue.isEmpty)
293+
assert(self.requests.isEmpty)
289294
let connection = self.connections.closeConnection(at: index)
290295
if self.connections.isEmpty {
291296
return .init(
@@ -308,15 +313,15 @@ extension HTTPConnectionPool {
308313
context: HTTP1Connections.IdleConnectionContext
309314
) -> Action {
310315
// 1. Check if there are waiting requests in the general purpose queue
311-
if let request = self.queue.popFirst(for: nil) {
316+
if let request = self.requests.popFirst(for: nil) {
312317
return .init(
313318
request: .executeRequest(request, self.connections.leaseConnection(at: index), cancelTimeout: true),
314319
connection: .none
315320
)
316321
}
317322

318323
// 2. Check if there are waiting requests in the matching eventLoop queue
319-
if let request = self.queue.popFirst(for: context.eventLoop) {
324+
if let request = self.requests.popFirst(for: context.eventLoop) {
320325
return .init(
321326
request: .executeRequest(request, self.connections.leaseConnection(at: index), cancelTimeout: true),
322327
connection: .none
@@ -337,7 +342,7 @@ extension HTTPConnectionPool {
337342
context: HTTP1Connections.IdleConnectionContext
338343
) -> Action {
339344
// Check if there are waiting requests in the matching eventLoop queue
340-
if let request = self.queue.popFirst(for: context.eventLoop) {
345+
if let request = self.requests.popFirst(for: context.eventLoop) {
341346
return .init(
342347
request: .executeRequest(request, self.connections.leaseConnection(at: index), cancelTimeout: true),
343348
connection: .none
@@ -372,7 +377,7 @@ extension HTTPConnectionPool {
372377
}
373378

374379
case .shuttingDown(let unclean):
375-
assert(self.queue.isEmpty)
380+
assert(self.requests.isEmpty)
376381
self.connections.removeConnection(at: index)
377382
if self.connections.isEmpty {
378383
return .init(
@@ -391,7 +396,7 @@ extension HTTPConnectionPool {
391396
at index: Int,
392397
context: HTTP1Connections.FailedConnectionContext
393398
) -> Action {
394-
if context.connectionsStartingForUseCase < self.queue.count(for: nil) {
399+
if context.connectionsStartingForUseCase < self.requests.generalPurposeCount {
395400
// if we have more requests queued up, than we have starting connections, we should
396401
// create a new connection
397402
let (newConnectionID, newEventLoop) = self.connections.replaceConnection(at: index)
@@ -408,7 +413,7 @@ extension HTTPConnectionPool {
408413
at index: Int,
409414
context: HTTP1Connections.FailedConnectionContext
410415
) -> Action {
411-
if context.connectionsStartingForUseCase < self.queue.count(for: context.eventLoop) {
416+
if context.connectionsStartingForUseCase < self.requests.count(for: context.eventLoop) {
412417
// if we have more requests queued up, than we have starting connections, we should
413418
// create a new connection
414419
let (newConnectionID, newEventLoop) = self.connections.replaceConnection(at: index)
@@ -421,8 +426,8 @@ extension HTTPConnectionPool {
421426
return .none
422427
}
423428

424-
private func calculateBackoff(for attempt: Int) -> TimeAmount {
425-
// Our backoff formula is: 100ms * 1.25^attempt that is capped of at 1minute
429+
private func calculateBackoff(failedAttempt attempts: Int) -> TimeAmount {
430+
// Our backoff formula is: 100ms * 1.25^(attempts - 1) that is capped of at 1minute
426431
// This means for:
427432
// - 1 failed attempt : 100ms
428433
// - 5 failed attempts: ~300ms
@@ -433,7 +438,7 @@ extension HTTPConnectionPool {
433438
// - 29 failed attempts: ~60s (max out)
434439

435440
let start = Double(TimeAmount.milliseconds(100).nanoseconds)
436-
let backoffNanoseconds = Int64(start * pow(1.25, Double(attempt)))
441+
let backoffNanoseconds = Int64(start * pow(1.25, Double(attempts - 1)))
437442

438443
let backoff: TimeAmount = min(.nanoseconds(backoffNanoseconds), .seconds(60))
439444

@@ -450,7 +455,7 @@ extension HTTPConnectionPool {
450455
extension HTTPConnectionPool.HTTP1StateMachine: CustomStringConvertible {
451456
var description: String {
452457
let stats = self.connections.stats
453-
let queued = self.queue.count
458+
let queued = self.requests.count
454459

455460
return "connections: [connecting: \(stats.connecting) | backoff: \(stats.backingOff) | leased: \(stats.leased) | idle: \(stats.idle)], queued: \(queued)"
456461
}

Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+RequestQueue.swift

+6-5
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ extension HTTPConnectionPool {
3333
self.count == 0
3434
}
3535

36-
func count(for eventLoop: EventLoop?) -> Int {
37-
if let eventLoop = eventLoop {
38-
return self.withEventLoopQueueIfAvailable(for: eventLoop.id) { $0.count } ?? 0
39-
}
40-
return self.generalPurposeQueue.count
36+
var generalPurposeCount: Int {
37+
self.generalPurposeQueue.count
38+
}
39+
40+
func count(for eventLoop: EventLoop) -> Int {
41+
self.withEventLoopQueueIfAvailable(for: eventLoop.id) { $0.count } ?? 0
4142
}
4243

4344
func isEmpty(for eventLoop: EventLoop?) -> Bool {

Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+StateMachine.swift

+3-5
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ extension HTTPConnectionPool {
6060
case none
6161
}
6262

63-
enum HTTPTypeStateMachine {
63+
enum HTTPVersionState {
6464
case http1(HTTP1StateMachine)
6565
}
6666

67-
var state: HTTPTypeStateMachine
67+
var state: HTTPVersionState
6868
var isShuttingDown: Bool = false
6969

7070
let eventLoopGroup: EventLoopGroup
@@ -176,9 +176,7 @@ extension HTTPConnectionPool {
176176
}
177177

178178
mutating func shutdown() -> Action {
179-
guard !self.isShuttingDown else {
180-
preconditionFailure("Shutdown must only be called once")
181-
}
179+
precondition(!self.isShuttingDown, "Shutdown must only be called once")
182180

183181
self.isShuttingDown = true
184182

Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP1ConnectionsTest.swift

+9-3
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
6868
let backoff1EL = connections.backoffNextConnectionAttempt(conn1ID)
6969
XCTAssert(backoff1EL === el1)
7070
// backoff done. 2. decide what's next
71-
let (conn1FailIndex, conn1FailContext) = connections.failConnection(conn1ID)
71+
guard let (conn1FailIndex, conn1FailContext) = connections.failConnection(conn1ID) else {
72+
return XCTFail("Expected that the connection is remembered")
73+
}
7274
XCTAssert(conn1FailContext.eventLoop === el1)
7375
XCTAssertEqual(conn1FailContext.use, .generalPurpose)
7476
XCTAssertEqual(conn1FailContext.connectionsStartingForUseCase, 0)
@@ -83,7 +85,9 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
8385
XCTAssertEqual(connections.startingEventLoopConnections(on: el2), 1)
8486
let backoff2EL = connections.backoffNextConnectionAttempt(conn2ID)
8587
XCTAssert(backoff2EL === el2)
86-
let (conn2FailIndex, conn2FailContext) = connections.failConnection(conn2ID)
88+
guard let (conn2FailIndex, conn2FailContext) = connections.failConnection(conn2ID) else {
89+
return XCTFail("Expected that the connection is remembered")
90+
}
8791
XCTAssert(conn2FailContext.eventLoop === el2)
8892
XCTAssertEqual(conn2FailContext.use, .eventLoop(el2))
8993
XCTAssertEqual(conn2FailContext.connectionsStartingForUseCase, 0)
@@ -329,7 +333,9 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
329333
XCTAssertEqual(connections.closeConnection(at: releaseIndex), lease)
330334
XCTAssertFalse(connections.isEmpty)
331335

332-
let (failIndex, _) = connections.failConnection(startingID)
336+
guard let (failIndex, _) = connections.failConnection(startingID) else {
337+
return XCTFail("Expected that the connection is remembered")
338+
}
333339
connections.removeConnection(at: failIndex)
334340
XCTAssertTrue(connections.isEmpty)
335341
}

0 commit comments

Comments
 (0)