-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
connection: adding watermarks to the read buffer. #11170
Conversation
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
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.
Super excited about this change to extend watermark buffer logic to connection read buffers. Thanks for putting this together.
@@ -38,6 +38,7 @@ class WatermarkBuffer : public OwnedImpl { | |||
void setWatermarks(uint32_t watermark) { setWatermarks(watermark / 2, watermark); } | |||
void setWatermarks(uint32_t low_watermark, uint32_t high_watermark); | |||
uint32_t highWatermark() const { return high_watermark_; } | |||
bool aboveHighWatermark() const { return above_high_watermark_called_; } |
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.
nit: This returns true if the high watermark is triggered and the buffer size hasn't dropped below the low watermark.
- Could you add a comment for this function?
- Would highWatermarkTriggered be a better name?
if (consumerWantsToRead() && read_buffer_.length() > 0) { | ||
// If the connection has data buffered there's no guarantee there's also data in the kernel | ||
// which will kick off the filter chain. Instead fake an event to make sure the buffered data | ||
// gets processed regardless and ensure that we dispatch it via onRead. |
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.
It seems that this fake read event wakeup is delivered even in cases where the file_event does not have the Read mask set. Worth expanding this comment?
|
||
void ConnectionImpl::onReadBufferHighWatermark() { | ||
ENVOY_CONN_LOG(debug, "onAboveReadBufferHighWatermark", *this); | ||
readDisable(true); |
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.
"if (state() == State::Open) {" seems necessary here also.
I'm thinking about upstream responses over HTTPS where the last byte of the response read, EOF is detected, then SslSocket commits the reservation and triggers the high-watermark. (SslSocket is special; plaintext socket does 1 commit per readv call, while SslSocket does 1 commit per group of SSL_read calls used to fill the current reservation).
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'm happy to add a guard (you know how pro-error-handling I am) but I don't think it's necessary.
In the case we do SSL_read and pick up the fin, I believe we will do multiple reads, but in SslSocket::doRead we commit all the data (buffer goes high) and then return the action PostIoAction::Close, so retaining the invariant that we close after the read is done, and shouldn't add data to the connection after the socket has been closed.
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'm ok skipping the check.
if (read_disable_count_ != 0) { | ||
if (dispatch_buffered_data_ && consumerWantsToRead()) { | ||
onRead(read_buffer_.length()); | ||
dispatch_buffered_data_ = false; |
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 think you want to clear dispatch_buffered_data_ before calling onRead.
onRead may call readDisable(false) to re-enable as data is consumed and read buffer falls below low watermark. readDisable will set dispatch_buffered_data_ = true a second time, and this will then set it back to false resulting in a failed resumption if the fd read attempt after resumption returns 0 bytes but there are bytes in the read buffer.
The current "dispatch_buffered_data_ = false;" has a similar bug which may only trigger if you send 2 pipelined HTTP/1.1 GET requests, we do readDisable(true) after reading headers for the first request, internal error response would terminate the first request and trigger readDisable(false) which would set dispatch_buffered_data_ and fd ready for resumption, but resumption would be ignored because dispatch_buffered_data_ is unset when onRead returns.
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.
Oh, that's a really nice catch.
Looking closely at the code, I was replicating the buggy logic from down below (583) where the same thing could happen on the actual socket read path. I was going to bump both calls above the onRead, but then the lower case doesn't clear dispatch_buffered_data consistently, so I think I'm just going to latch-and-clear at the top of the function to attempt to future-proof.
dispatcher_->exit(); | ||
return FilterStatus::StopIteration; | ||
})); | ||
dispatcher_->run(Event::Dispatcher::RunType::Block); |
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.
Worth doing a second server_connection write before doing drains, and verify that there is no read from the client connection on dispatcher_->run()?
{ | ||
client_connection_->readDisable(true); | ||
testClientConnection()->readBuffer().drain(3); | ||
EXPECT_FALSE(testClientConnection()->readBuffer().aboveHighWatermark()); |
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.
Worth adding some consumerWantsToRead and dispatch_buffered_data_ expectations?
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.
Did another pass and the changes look good. You may want to reply to and close some of the open comment threads.
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.
Nice, makes sense to me. Just a few small comments. Thank you!
/wait
@@ -122,11 +122,19 @@ class ConnectionImpl : public ConnectionImplBase, public TransportSocketCallback | |||
static uint64_t nextGlobalIdForTest() { return next_global_id_; } | |||
|
|||
protected: | |||
// A convenience function which returns true if | |||
// 1) The read disable count is zero or | |||
// 2) The read disable count is one, due to the read buffer being overrun. |
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.
Isn't there a chance that the read disable count is > 1 and the buffer is overrun? Like if it got overrun but also disabled some other way? Or is the idea that if it is > 1 we will wait until the only reason is it's disabled due to read overrun? If that's the case can you add more comments just to make it completely clear?
@@ -342,24 +350,24 @@ void ConnectionImpl::readDisable(bool disable) { | |||
} | |||
} else { | |||
--read_disable_count_; |
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.
While you are here can you ASSERT this is greater than 0 before decrementing? I'm surprised this was not already asserted.
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
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!
Fixing an issue where every time a connection was readDisabled/readEnabled it would read from the socket, even if the buffer already contained sufficient data it should have triggered push back.
Risk Level: high (watermarks)
Testing: new unit tests
Docs Changes: n/a
Release Notes: n/a
Fixes https://github.com/envoyproxy/envoy-setec/issues/90 (no longer a security risk after previous mitigation)