-
Notifications
You must be signed in to change notification settings - Fork 125
Call didSendRequestPart
at the right time
#566
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
Conversation
Can one of the admins verify this patch? |
7 similar comments
Can one of the admins verify this patch? |
Can one of the admins verify this patch? |
Can one of the admins verify this patch? |
Can one of the admins verify this patch? |
Can one of the admins verify this patch? |
Can one of the admins verify this patch? |
Can one of the admins verify this patch? |
ef29eb0
to
ebafa1e
Compare
@swift-nio-bot add to allowlist |
a60554e
to
5780c10
Compare
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.
There needs to be much clearer auditing about who is responsible for succeeding this promise. A bunch of these calls return without either storing the promise or passing it on, and in those cases we risk leaking the promise without succeeding it. That all needs to be updated.
@Lukasa Thanks for your review. You absolutely write I just dropped the promise in a couple of places. I audited the code paths again and made sure to fail the promise in the other branches and added some tests. The only thing that I wasn't really sure about is what errors to return in some places. Looking forward to suggestions! |
Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
Outdated
Show resolved
Hide resolved
@@ -965,6 +966,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { | |||
public static let readTimeout = HTTPClientError(code: .readTimeout) | |||
/// Remote connection was closed unexpectedly. | |||
public static let remoteConnectionClosed = HTTPClientError(code: .remoteConnectionClosed) | |||
/// Request failed. | |||
public static let failed = HTTPClientError(code: .failed) |
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.
Can we call this requestFailed
instead of just failed
?
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.
I was aligning it with the naming of cancelled
which is documented as a cancelled request. What do you prefer?
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 document, that we use this failure, to fail write promises on failed requests. This shall not be used as a general purpose error, even though the name is general purpose.
self.delegate.didSendRequestPart(task: self.task, part) | ||
let promise = self.task.eventLoop.makePromise(of: Void.self) | ||
promise.futureResult.whenSuccess { | ||
self.delegate.didSendRequestPart(task: self.task, part) |
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.
Should we report an error to the delegate in the failure case?
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.
Was thinking about that as well. Open to input here. @weissi For your backpressure implementation would this be valuable?
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.
We should make sure the error delegate is only called once.
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.
Should we report an error to the delegate in the failure case?
I don't think we should. All errors are reported to the error function.
47c3087
to
373792c
Compare
373792c
to
b549617
Compare
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.
LGTM but I'd like to see @fabianfett or @dnadoba review.
The API breakage is failing because of the two new protocol requirements. However, I added default implementations in an extension so this is not a breaking change. |
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.
Thanks so much for driving this. Left a bunch of comments.
Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
Outdated
Show resolved
Hide resolved
requestHandler.writeRequestBodyPart( | ||
.byteBuffer(.init()), | ||
request: requestBag, | ||
promise: promise | ||
) |
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.
This tests doesn't make any sense to me. The requestHandler
is not on an EmbeddedChannel
. I think what we actually want to test here, is the delegate called, once and only once the write has hit the beginning of the ChannelPipeline. For this you will need a handler that holds the write for a bit, before it is allowed to let the writes through.
XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .none)) | ||
XCTAssertEqual(state.requestStreamPartReceived(part, promise: promise), .wait, | ||
"Expected to drop all stream data after having received a response head, with status >= 300") | ||
XCTAssertThrowsError(try promise.futureResult.wait()) |
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 verify the correct error here.
if self.shouldFailWrites { | ||
promise?.fail(ChannelError.eof) | ||
} else { | ||
promise?.succeed(()) | ||
} |
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.
You just passed the promise to another internal write method, but then fail it or succeed it here? That looks wrong.
82d51cb
to
944acc6
Compare
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.
Looks much better now. However we should change one more critical piece.
case .wait: | ||
break | ||
case .wait(let promise): | ||
promise?.fail(ChannelError.eof) |
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.
Urgh. I feel like this is a little to wild...
- I don't think we should add a hidden an action into the
wait
/none
action. I think we should add a special action for this case, instead of extendingwait
. - Why
ChannelError.eof
? I think we should create a new internal error for this.HTTPRequestUploadCancelledBecauseResponseGT300
. Also the error should be created in the state machine and handed out from the state machine in the new action with the promise.
case .wait: | ||
break | ||
case .wait(let promise): | ||
promise?.fail(ChannelError.eof) |
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.
See HTTP2 handler.
self.delegate.didSendRequestPart(task: self.task, part) | ||
let promise = self.task.eventLoop.makePromise(of: Void.self) | ||
promise.futureResult.whenSuccess { | ||
self.delegate.didSendRequestPart(task: self.task, part) |
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.
Should we report an error to the delegate in the failure case?
I don't think we should. All errors are reported to the error function.
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.
Found some more things. But this is starting to look, really good!
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ConnectionStateMachine.swift
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ConnectionStateMachine.swift
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ConnectionStateMachine.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ClientChannelHandler.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ClientChannelHandler.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ClientChannelHandler.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ClientChannelHandler.swift
Outdated
Show resolved
Hide resolved
case .forwardStreamFinished(let writer, let writerPromise): | ||
let promise = self.task.eventLoop.makePromise(of: Void.self) | ||
promise.futureResult.whenSuccess { | ||
self.delegate.didSendRequest(task: self.task) | ||
} | ||
writer.finishRequestBodyStream(self, promise: promise) | ||
writerPromise?.succeed(()) |
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.
Why do we create a new promise? Why is the writePromise just succeeded? This looks odd. This needs at least a comment.
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.
I thought that we need a new promise here but I just tested it again and with using the same promise and it worked.
a1c2ebf
to
926e482
Compare
926e482
to
76b051a
Compare
oldRequest.succeedRequest(buffer) | ||
} | ||
|
||
context.close(promise: writePromise) |
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.
@fabianfett Is it okay to pass the writePromise
to close
here?
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) | ||
case .informConnectionIsIdle: | ||
case .close(let writePromise): | ||
context.close(promise: writePromise) |
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.
Same question here @fabianfett ?
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.
I don't think it makes sense to pass the writePromise to close. Closing the connection really just concerns the standing connection and not the running request. failing the promise below is correct IMO.
8cbc71c
to
2d28426
Compare
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.
And another round!
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) | ||
case .informConnectionIsIdle: | ||
case .close(let writePromise): | ||
context.close(promise: writePromise) |
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.
I don't think it makes sense to pass the writePromise to close. Closing the connection really just concerns the standing connection and not the running request. failing the promise below is correct IMO.
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ClientChannelHandler.swift
Outdated
Show resolved
Hide resolved
case failRequest(Error, FinalStreamAction) | ||
case succeedRequest(FinalStreamAction, CircularBuffer<ByteBuffer>) | ||
case failRequest(Error, FinalFailedStreamAction) | ||
case succeedRequest(FinalSuccessfulStreamAction, CircularBuffer<ByteBuffer>) |
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 an issue introduced by you. Urgh. I think we should change the order here (first buffer, than last action). Maybe as a follow up.
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ConnectionStateMachine.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
Outdated
Show resolved
Hide resolved
# Motivation Right now, we call `didSendRequestPart` after passing the write to the executor. However, this does not mean that the write hit the socket. To implement proper backpressure using the delegate, we should only call this method once the write was successful. # Modification Pass a promise to the actual channel write and only call the delegate once that promise succeeds. # Result The delegate method `didSendRequestPart` is only called after the write was successful.
2d28426
to
85bb24d
Compare
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.
Nearly done!
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ClientChannelHandler.swift
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ConnectionStateMachine.swift
Outdated
Show resolved
Hide resolved
Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ConnectionStateMachine.swift
Outdated
Show resolved
Hide resolved
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.
Thank you! This has been a ton of work!
Motivation
Right now, we call
didSendRequestPart
after passing the write to the executor. However, this does not mean that the write hit the socket. To implement proper backpressure using the delegate, we should only call this method once the write was successful.Modification
Pass a promise to the actual channel write and only call the delegate once that promise succeeds.
Result
The delegate method
didSendRequestPart
is only called after the write was successful.