-
Notifications
You must be signed in to change notification settings - Fork 125
Report last connection error if request deadline is exceeded #601
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 5 commits
4c5733d
4243ab8
34f8a34
0477ef9
5e57585
8cbeeea
c7640da
05b329e
5b88cc8
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 |
---|---|---|
|
@@ -323,9 +323,10 @@ extension HTTPConnectionPool { | |
|
||
mutating func cancelRequest(_ requestID: Request.ID) -> Action { | ||
// 1. check requests in queue | ||
if self.requests.remove(requestID) != nil { | ||
if let request = self.requests.remove(requestID) { | ||
let error = self.lastConnectFailure ?? HTTPClientError.cancelled | ||
return .init( | ||
request: .cancelRequestTimeout(requestID), | ||
request: .failRequest(request, error, cancelTimeout: true), | ||
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. Since, we remove |
||
connection: .none | ||
) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,8 @@ extension RequestBag { | |
fileprivate enum State { | ||
case initialized | ||
case queued(HTTPRequestScheduler) | ||
/// if the deadline was exceeded while in the `.queued(_:)` state, we wait until the request pool fails the request with a potential more descriptive error message if a connection failure has occured while the request was queued. | ||
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. NIT: can we wrap the comment here? I think normally I wrapped at about 100 characters. |
||
case deadlineExceededWhileQueued | ||
case executing(HTTPRequestExecutor, RequestStreamState, ResponseStreamState) | ||
case finished(error: Error?) | ||
case redirected(HTTPRequestExecutor, Int, HTTPResponseHead, URL) | ||
|
@@ -95,7 +97,7 @@ extension RequestBag.StateMachine { | |
case .initialized, .queued: | ||
self.state = .executing(executor, .initialized, .initialized) | ||
return true | ||
case .finished(error: .some): | ||
case .finished(error: .some), .deadlineExceededWhileQueued: | ||
return false | ||
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. We'll need to fail the request here explicitly... |
||
case .executing, .redirected, .finished(error: .none), .modifying: | ||
preconditionFailure("Invalid state: \(self.state)") | ||
|
@@ -110,7 +112,7 @@ extension RequestBag.StateMachine { | |
|
||
mutating func resumeRequestBodyStream() -> ResumeProducingAction { | ||
switch self.state { | ||
case .initialized, .queued: | ||
case .initialized, .queued, .deadlineExceededWhileQueued: | ||
preconditionFailure("A request stream can only be resumed, if the request was started") | ||
|
||
case .executing(let executor, .initialized, .initialized): | ||
|
@@ -150,7 +152,7 @@ extension RequestBag.StateMachine { | |
|
||
mutating func pauseRequestBodyStream() { | ||
switch self.state { | ||
case .initialized, .queued: | ||
case .initialized, .queued, .deadlineExceededWhileQueued: | ||
preconditionFailure("A request stream can only be paused, if the request was started") | ||
case .executing(let executor, let requestState, let responseState): | ||
switch requestState { | ||
|
@@ -185,7 +187,7 @@ extension RequestBag.StateMachine { | |
|
||
mutating func writeNextRequestPart(_ part: IOData, taskEventLoop: EventLoop) -> WriteAction { | ||
switch self.state { | ||
case .initialized, .queued: | ||
case .initialized, .queued, .deadlineExceededWhileQueued: | ||
preconditionFailure("Invalid state: \(self.state)") | ||
case .executing(let executor, let requestState, let responseState): | ||
switch requestState { | ||
|
@@ -231,7 +233,7 @@ extension RequestBag.StateMachine { | |
|
||
mutating func finishRequestBodyStream(_ result: Result<Void, Error>) -> FinishAction { | ||
switch self.state { | ||
case .initialized, .queued: | ||
case .initialized, .queued, .deadlineExceededWhileQueued: | ||
preconditionFailure("Invalid state: \(self.state)") | ||
case .executing(let executor, let requestState, let responseState): | ||
switch requestState { | ||
|
@@ -282,7 +284,7 @@ extension RequestBag.StateMachine { | |
/// - Returns: Whether the response should be forwarded to the delegate. Will be `false` if the request follows a redirect. | ||
mutating func receiveResponseHead(_ head: HTTPResponseHead) -> ReceiveResponseHeadAction { | ||
switch self.state { | ||
case .initialized, .queued: | ||
case .initialized, .queued, .deadlineExceededWhileQueued: | ||
preconditionFailure("How can we receive a response, if the request hasn't started yet.") | ||
case .executing(let executor, let requestState, let responseState): | ||
guard case .initialized = responseState else { | ||
|
@@ -328,7 +330,7 @@ extension RequestBag.StateMachine { | |
|
||
mutating func receiveResponseBodyParts(_ buffer: CircularBuffer<ByteBuffer>) -> ReceiveResponseBodyAction { | ||
switch self.state { | ||
case .initialized, .queued: | ||
case .initialized, .queued, .deadlineExceededWhileQueued: | ||
preconditionFailure("How can we receive a response body part, if the request hasn't started yet.") | ||
case .executing(_, _, .initialized): | ||
preconditionFailure("If we receive a response body, we must have received a head before") | ||
|
@@ -385,7 +387,7 @@ extension RequestBag.StateMachine { | |
|
||
mutating func succeedRequest(_ newChunks: CircularBuffer<ByteBuffer>?) -> ReceiveResponseEndAction { | ||
switch self.state { | ||
case .initialized, .queued: | ||
case .initialized, .queued, .deadlineExceededWhileQueued: | ||
preconditionFailure("How can we receive a response body part, if the request hasn't started yet.") | ||
case .executing(_, _, .initialized): | ||
preconditionFailure("If we receive a response body, we must have received a head before") | ||
|
@@ -447,7 +449,7 @@ extension RequestBag.StateMachine { | |
|
||
private mutating func failWithConsumptionError(_ error: Error) -> ConsumeAction { | ||
switch self.state { | ||
case .initialized, .queued: | ||
case .initialized, .queued, .deadlineExceededWhileQueued: | ||
preconditionFailure("Invalid state: \(self.state)") | ||
case .executing(_, _, .initialized): | ||
preconditionFailure("Invalid state: Must have received response head, before this method is called for the first time") | ||
|
@@ -482,7 +484,7 @@ extension RequestBag.StateMachine { | |
|
||
private mutating func consumeMoreBodyData() -> ConsumeAction { | ||
switch self.state { | ||
case .initialized, .queued: | ||
case .initialized, .queued, .deadlineExceededWhileQueued: | ||
preconditionFailure("Invalid state: \(self.state)") | ||
|
||
case .executing(_, _, .initialized): | ||
|
@@ -532,8 +534,23 @@ extension RequestBag.StateMachine { | |
} | ||
} | ||
|
||
enum DeadlineExceededAction { | ||
case cancelScheduler(HTTPRequestScheduler?) | ||
case fail(FailAction) | ||
} | ||
|
||
mutating func deadlineExceeded() -> DeadlineExceededAction { | ||
switch self.state { | ||
case .queued(let queuer): | ||
self.state = .deadlineExceededWhileQueued | ||
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 would love a comment here, why we do this dance. Please also make explicit that we depend on the scheduler to fail this request. What happens, however if we think we are still queued, but we aren't actually? There might be a race here:
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. For this we def. need a test case as well. |
||
return .cancelScheduler(queuer) | ||
default: | ||
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. Can we make all the other cases explicit here? |
||
return .fail(self.fail(HTTPClientError.deadlineExceeded)) | ||
dnadoba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
enum FailAction { | ||
case failTask(HTTPRequestScheduler?, HTTPRequestExecutor?) | ||
case failTask(Error, HTTPRequestScheduler?, HTTPRequestExecutor?) | ||
case cancelExecutor(HTTPRequestExecutor) | ||
case none | ||
} | ||
|
@@ -542,31 +559,39 @@ extension RequestBag.StateMachine { | |
switch self.state { | ||
case .initialized: | ||
self.state = .finished(error: error) | ||
return .failTask(nil, nil) | ||
return .failTask(error, nil, nil) | ||
case .queued(let queuer): | ||
self.state = .finished(error: error) | ||
return .failTask(queuer, nil) | ||
return .failTask(error, queuer, nil) | ||
case .executing(let executor, let requestState, .buffering(_, next: .eof)): | ||
self.state = .executing(executor, requestState, .buffering(.init(), next: .error(error))) | ||
return .cancelExecutor(executor) | ||
case .executing(let executor, _, .buffering(_, next: .askExecutorForMore)): | ||
self.state = .finished(error: error) | ||
return .failTask(nil, executor) | ||
return .failTask(error, nil, executor) | ||
case .executing(let executor, _, .buffering(_, next: .error(_))): | ||
// this would override another error, let's keep the first one | ||
return .cancelExecutor(executor) | ||
case .executing(let executor, _, .initialized): | ||
self.state = .finished(error: error) | ||
return .failTask(nil, executor) | ||
return .failTask(error, nil, executor) | ||
case .executing(let executor, _, .waitingForRemote): | ||
self.state = .finished(error: error) | ||
return .failTask(nil, executor) | ||
return .failTask(error, nil, executor) | ||
case .redirected: | ||
self.state = .finished(error: error) | ||
return .failTask(nil, nil) | ||
return .failTask(error, nil, nil) | ||
case .finished(.none): | ||
// An error occurred after the request has finished. Ignore... | ||
return .none | ||
case .deadlineExceededWhileQueued: | ||
// if we just get a `HTTPClientError.cancelled` we can use the orignal cancelation reason | ||
dnadoba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// to give a more descriptive error to the user. | ||
if (error as? HTTPClientError) == .cancelled { | ||
return .failTask(HTTPClientError.deadlineExceeded, nil, nil) | ||
} | ||
// otherwise we already had an intermidate connection error which we should present to the user instead | ||
dnadoba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return .failTask(error, nil, nil) | ||
case .finished(.some(_)): | ||
// this might happen, if the stream consumer has failed... let's just drop the data | ||
return .none | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a comment here, why we look into the
lastConnectFailure
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory we could check here if we are at max connections... In those cases we could issue a more explicit:
deadlineExceededBecauseToQueueDepthToLong
error.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how useful this kind of error message would be because
deadlineExceededBecauseToQueueDepthToLong
may only be a symptom as other requests in front could not be scheduled because there was a connection failure.