-
Notifications
You must be signed in to change notification settings - Fork 125
Allow immediate request failure on connection error #625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ab6142b
cfce59e
de02ba3
2cff156
8a1f97d
67a7c6d
06bb497
8046ab3
6890dbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import NIOCore | |
extension HTTPConnectionPool { | ||
struct HTTP1StateMachine { | ||
typealias Action = HTTPConnectionPool.StateMachine.Action | ||
typealias RequestAction = HTTPConnectionPool.StateMachine.RequestAction | ||
typealias ConnectionMigrationAction = HTTPConnectionPool.StateMachine.ConnectionMigrationAction | ||
typealias EstablishedAction = HTTPConnectionPool.StateMachine.EstablishedAction | ||
typealias EstablishedConnectionAction = HTTPConnectionPool.StateMachine.EstablishedConnectionAction | ||
|
@@ -29,16 +30,21 @@ extension HTTPConnectionPool { | |
|
||
private(set) var requests: RequestQueue | ||
private(set) var lifecycleState: StateMachine.LifecycleState | ||
/// The property was introduced to fail fast during testing. | ||
/// Otherwise this should always be true and not turned off. | ||
private let retryConnectionEstablishment: Bool | ||
|
||
init( | ||
idGenerator: Connection.ID.Generator, | ||
maximumConcurrentConnections: Int, | ||
retryConnectionEstablishment: Bool, | ||
lifecycleState: StateMachine.LifecycleState | ||
) { | ||
self.connections = HTTP1Connections( | ||
maximumConcurrentConnections: maximumConcurrentConnections, | ||
generator: idGenerator | ||
) | ||
self.retryConnectionEstablishment = retryConnectionEstablishment | ||
|
||
self.requests = RequestQueue() | ||
self.lifecycleState = lifecycleState | ||
|
@@ -219,6 +225,17 @@ extension HTTPConnectionPool { | |
|
||
switch self.lifecycleState { | ||
case .running: | ||
guard self.retryConnectionEstablishment else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using the guard here it might be nicer to go with where clause in the switch... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A where constraint only applies to the last case if multiple cases are defined e.g. case .running, .shuttingDown where self.retryConnectionEstablishment == true: the where constrain only applies to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. true, however here we only have one case here. Anyway, not a hill I want to die on. If we want to stick with an inline if/guard, I would prefer this to be an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the benefit of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I consider it nicer to read. But that's personal... |
||
guard let (index, _) = self.connections.failConnection(connectionID) else { | ||
preconditionFailure("A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost.") | ||
} | ||
self.connections.removeConnection(at: index) | ||
|
||
return .init( | ||
request: self.failAllRequests(reason: error), | ||
connection: .none | ||
) | ||
} | ||
// We don't care how many waiting requests we have at this point, we will schedule a | ||
// retry. More tasks, may appear until the backoff has completed. The final | ||
// decision about the retry will be made in `connectionCreationBackoffDone(_:)` | ||
|
@@ -523,6 +540,14 @@ extension HTTPConnectionPool { | |
return .none | ||
} | ||
|
||
private mutating func failAllRequests(reason error: Error) -> RequestAction { | ||
let allRequests = self.requests.removeAll() | ||
guard !allRequests.isEmpty else { | ||
return .none | ||
} | ||
return .failRequestsAndCancelTimeouts(allRequests, error) | ||
} | ||
|
||
// MARK: HTTP2 | ||
|
||
mutating func newHTTP2MaxConcurrentStreamsReceived(_ connectionID: Connection.ID, newMaxStreams: Int) -> Action { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ import NIOHTTP2 | |
extension HTTPConnectionPool { | ||
struct HTTP2StateMachine { | ||
typealias Action = HTTPConnectionPool.StateMachine.Action | ||
typealias RequestAction = HTTPConnectionPool.StateMachine.RequestAction | ||
typealias ConnectionMigrationAction = HTTPConnectionPool.StateMachine.ConnectionMigrationAction | ||
typealias EstablishedAction = HTTPConnectionPool.StateMachine.EstablishedAction | ||
typealias EstablishedConnectionAction = HTTPConnectionPool.StateMachine.EstablishedConnectionAction | ||
|
@@ -33,16 +34,21 @@ extension HTTPConnectionPool { | |
private let idGenerator: Connection.ID.Generator | ||
|
||
private(set) var lifecycleState: StateMachine.LifecycleState | ||
/// The property was introduced to fail fast during testing. | ||
/// Otherwise this should always be true and not turned off. | ||
private let retryConnectionEstablishment: Bool | ||
dnadoba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
init( | ||
idGenerator: Connection.ID.Generator, | ||
retryConnectionEstablishment: Bool, | ||
lifecycleState: StateMachine.LifecycleState | ||
) { | ||
self.idGenerator = idGenerator | ||
self.requests = RequestQueue() | ||
|
||
self.connections = HTTP2Connections(generator: idGenerator) | ||
self.lifecycleState = lifecycleState | ||
self.retryConnectionEstablishment = retryConnectionEstablishment | ||
} | ||
|
||
mutating func migrateFromHTTP1( | ||
|
@@ -398,16 +404,30 @@ extension HTTPConnectionPool { | |
} | ||
|
||
mutating func failedToCreateNewConnection(_ error: Error, connectionID: Connection.ID) -> Action { | ||
// TODO: switch over state https://github.com/swift-server/async-http-client/issues/638 | ||
self.failedConsecutiveConnectionAttempts += 1 | ||
self.lastConnectFailure = error | ||
|
||
guard self.retryConnectionEstablishment else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why don't we check the current running state here? Like in the http1 case? This seems weird. I think we should add this here. If the pool is closing, we should not backoff anymore. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah agree this is weird but has nothing to do with this change as we don't check the state at all. We should tackle this in a separate PR as we likely want to add test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
guard let (index, _) = self.connections.failConnection(connectionID) else { | ||
preconditionFailure("A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost.") | ||
} | ||
self.connections.removeConnection(at: index) | ||
|
||
return .init( | ||
request: self.failAllRequests(reason: error), | ||
connection: .none | ||
) | ||
} | ||
|
||
let eventLoop = self.connections.backoffNextConnectionAttempt(connectionID) | ||
let backoff = calculateBackoff(failedAttempt: self.failedConsecutiveConnectionAttempts) | ||
return .init(request: .none, connection: .scheduleBackoffTimer(connectionID, backoff: backoff, on: eventLoop)) | ||
} | ||
|
||
mutating func waitingForConnectivity(_ error: Error, connectionID: Connection.ID) -> Action { | ||
self.lastConnectFailure = error | ||
|
||
return .init(request: .none, connection: .none) | ||
} | ||
|
||
|
@@ -421,6 +441,14 @@ extension HTTPConnectionPool { | |
return self.nextActionForFailedConnection(at: index, on: context.eventLoop) | ||
} | ||
|
||
private mutating func failAllRequests(reason error: Error) -> RequestAction { | ||
let allRequests = self.requests.removeAll() | ||
guard !allRequests.isEmpty else { | ||
return .none | ||
} | ||
return .failRequestsAndCancelTimeouts(allRequests, error) | ||
} | ||
|
||
mutating func timeoutRequest(_ requestID: Request.ID) -> Action { | ||
// 1. check requests in queue | ||
if let request = self.requests.remove(requestID) { | ||
|
Uh oh!
There was an error while loading. Please reload this page.