quiche: implement http stream interfaces #8556
quiche: implement http stream interfaces #8556alyssawilk merged 41 commits intoenvoyproxy:masterfrom
Conversation
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
This reverts commit 3e39fbe. Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
|
This one is ready for review. PTAL |
alyssawilk
left a comment
There was a problem hiding this comment.
Thanks for tackling this? Got partway through and I'll tackle the rest once we've worked out some docs
|
|
||
| void QuicHttpServerConnectionImpl::onUnderlyingConnectionAboveWriteBufferHighWatermark() { | ||
| for (auto& it : quic_server_session_.stream_map()) { | ||
| if (!it.second->is_static()) { |
There was a problem hiding this comment.
I am completely failing to find what is_static checks. can you clue me in?
There was a problem hiding this comment.
is_static is true for static streams, for IETF QUIC, it's crypto stream, and for GQuic, also headers stream.
There was a problem hiding this comment.
Nowadays, all the streams are managed in one place: QuicSession::stream_map_
There was a problem hiding this comment.
So we're willing to infinite buffer on headers stream? We had a recent attack on the H2 stack where when an incoming request could generate an immediate response (like a 400 or 404) the flow control "didn't work" because we had assumed downstream buffers were fed by upstream (backends) not downstream (new bad requests) and adversary could keep feeding bad requests, generating header-only responses and adding more and more to the downstream connection. I want to make sure we don't reintroduce that flaw for HTTP/3
One workaround would be #7619 to have an upper upper bound but that hasn't landed yet. The other was the series of PRs @yanavlasov did to cap outbound frames (search for "flood" in the h2 codec). I think we need one or the other here if headers are otherwise unbounded. TODO is fine.
If we leave this as is for now I'd suggest a comment with the TODO
"we do not yet cap bytes for static streams, like the gQUIC headers stream or the crypto stream" to make it clear what is_static means
There was a problem hiding this comment.
QuicHeaderStream always buffer data, its WriteHeaders() doesn't check this threshold. In IETF QUIC, there is no headers stream, and headers will be serialized into stream payload. That would solve this problem, right?
There was a problem hiding this comment.
Not having a headers stream helps, but it'd be good to have hard bounds on the crypto stream too.
Again maybe we can have a TODO which admits it will be solved by #1719 but as that's closed and not actively being worked on, I think we need something tracking it as an active problem
There was a problem hiding this comment.
If we need a hard bound on crypto stream, it needs to be in QUICHE because the handshake is hidden from envoy.
But I still don't understand how can crypto stream subject to this sort of attack?
There was a problem hiding this comment.
Checked QUICHE code, the IETF quic data stream should be robust against this sort of attack because the buffered headers in data stream's send buffer should have prevented stream from being closed still the header is ACK'ed. So the totally data buffered is capped by max allowed header size * max allowed stream number.
For GQuic, because all the headers are sent on header stream and stream is closed once header is buffered in send buffer, the header stream can buffer infinite number of response headers. We need a hard bound for headers stream. But this probably not worth to be added in QUICHE now as QUICHE is moving to IETF quic.
crypto stream also has infinite send buffer, if handshake implementation is wrong, it can also buffer infinitely. I added TODO in EnvoyQuicServerStream::encodeHeaders() about this vulnerability.
| // This is counting not serialized bytes in the send buffer. | ||
| uint64_t bytes_to_send_old = BufferedDataBytes(); | ||
| // QUIC stream must take all. | ||
| WriteBodySlices(quic::QuicMemSliceSpan(quic::QuicMemSliceSpanImpl(data)), end_stream); |
There was a problem hiding this comment.
I'm a bit confused about how this works.
I thought that WriteBodySlices consumed some amount of data, and returned how many bytes are consumed. Where are the rest going? Are they getting drained from data and then deleted as the transient QuicMemSlice bits go away or OnCanWrite?
I think this could benefit from some doccing up like in source/docs/flow_control.md talking about how things should work, and I can take another pass once we have it written up?
There was a problem hiding this comment.
Added docs/quiche_integration.md. PTAL
There was a problem hiding this comment.
It seems dangerous to ignore the return value of WriteBodySlices. Can we consider the follow behavior instead:
- Use a very large buffered_data_threshold. This threshold has little to do with upstream flow control window etc, it is only used to prevent pathological conditions.
- When we WriteBodySlices, we only consider it success if all data is consumed. Otherwise close the connection.
- After a successful WriteBodySlices, run the watermark callbacks if needed.
There was a problem hiding this comment.
added Reset(QUIC_BAD_APPLICATION_PAYLOAD) and early return.
| local_end_stream_ = end_stream; | ||
| // This is counting not serialized bytes in the send buffer. | ||
| uint64_t bytes_to_send_old = BufferedDataBytes(); | ||
| // QUIC stream must take all. |
There was a problem hiding this comment.
Are we setting buffered_data_threshold_ high enough we think this is guaranteed?
There was a problem hiding this comment.
yes, and that's why I assume WriteBodySlices() always consumes all the data.
There was a problem hiding this comment.
So the only relevant thing I find here is
EnvoyQuicStream(2 * GetQuicFlag(FLAGS_quic_buffered_data_threshold) ...which is
EnvoyQuicStream(2 * 8 * 1024
which doesn't look remotely high enough
what am I missing? maybe the docs can call out where it's set since it's not (to me) obvious?
There was a problem hiding this comment.
I didn't adjust that flag yet, because I did't have a good sense of how much we want to buffer. I set the flag to be 2 * max flow control window during QuicDispatcher construction now, considering that QUIC will continue encodeData() for up to 1 flow control window of data.
EnvoyQuicStream() here takes in the high watermark actually, it should be less than what the threshold allows to block upper stream before the buffer rejects taking more data. I changed it to be 32k for now.
| if (bytes_to_send_new > bytes_to_send_old) { | ||
| // If buffered bytes changed, update stream and session's watermark book | ||
| // keeping. | ||
| sendBufferSimulation().checkHighWatermark(bytes_to_send_new); |
There was a problem hiding this comment.
So we're going to adjust this when we try to encode new data, but do we also adjust it when the QUIC stack drains data from the buffer? Like if pacing is on, will we trigger low watermrks as we ship data? I'm not clear on where this happens.
There was a problem hiding this comment.
All the write is driven by OnCanWrite() where we call checkLowWatermark()
| // Network::WriteBufferSource | ||
| Network::StreamBuffer getWriteBuffer() override { NOT_REACHED_GCOVR_EXCL_LINE; } | ||
|
|
||
| void adjustBytesToSend(int64_t delta); |
alyssawilk
left a comment
There was a problem hiding this comment.
Cool, the docs are awesome! Sent docs fixups offline but here's another round for review.
|
|
||
| void QuicHttpServerConnectionImpl::onUnderlyingConnectionAboveWriteBufferHighWatermark() { | ||
| for (auto& it : quic_server_session_.stream_map()) { | ||
| if (!it.second->is_static()) { |
There was a problem hiding this comment.
So we're willing to infinite buffer on headers stream? We had a recent attack on the H2 stack where when an incoming request could generate an immediate response (like a 400 or 404) the flow control "didn't work" because we had assumed downstream buffers were fed by upstream (backends) not downstream (new bad requests) and adversary could keep feeding bad requests, generating header-only responses and adding more and more to the downstream connection. I want to make sure we don't reintroduce that flaw for HTTP/3
One workaround would be #7619 to have an upper upper bound but that hasn't landed yet. The other was the series of PRs @yanavlasov did to cap outbound frames (search for "flood" in the h2 codec). I think we need one or the other here if headers are otherwise unbounded. TODO is fine.
If we leave this as is for now I'd suggest a comment with the TODO
"we do not yet cap bytes for static streams, like the gQUIC headers stream or the crypto stream" to make it clear what is_static means
| local_end_stream_ = end_stream; | ||
| // This is counting not serialized bytes in the send buffer. | ||
| uint64_t bytes_to_send_old = BufferedDataBytes(); | ||
| // QUIC stream must take all. |
There was a problem hiding this comment.
So the only relevant thing I find here is
EnvoyQuicStream(2 * GetQuicFlag(FLAGS_quic_buffered_data_threshold) ...which is
EnvoyQuicStream(2 * 8 * 1024
which doesn't look remotely high enough
what am I missing? maybe the docs can call out where it's set since it's not (to me) obvious?
| ASSERT(data.length() == 0); | ||
|
|
||
| uint64_t bytes_to_send_new = BufferedDataBytes(); | ||
| ASSERT(bytes_to_send_old <= bytes_to_send_new); |
There was a problem hiding this comment.
sanity check on quic stack functionality - if we'd had buffered data above the high watermark, and QUIC pacing allowed us to write more (OnCanWrite was queued up in the dispatcher loop) and we get data from a backend and send WriteBodySlices, could we immediately write, and so drain, and have fewer bytes to send?
As an aside, it's weird to me we'd have an assert and immediately after we'd have the if. I wonder if we should have a wrapper function checkWatermarks(bytes_to_send_old, bytes_to_send_new)
which simply returns if they're equal, and checks high watermarks if new > old and low if new < old and we can call checkWatermarks in all the places we're manually checking. Thoughts?
There was a problem hiding this comment.
sanity check on quic stack functionality - if we'd had buffered data above the high watermark, and QUIC pacing allowed us to write more (OnCanWrite was queued up in the dispatcher loop) and we get data from a backend and send WriteBodySlices, could we immediately write, and so drain, and have fewer bytes to send?
Yes, WriteBodySlices() will still write before OnCanWrite() get called via send_alarm in the queue. And OnCanWrite() might not able to write anymore afterwards as WriteBodySlices() already takes the chance to write.
As an aside, it's weird to me we'd have an assert and immediately after we'd have the if. I wonder if we should have a wrapper function checkWatermarks(bytes_to_send_old, bytes_to_send_new)
which simply returns if they're equal, and checks high watermarks if new > old and low if new < old and we can call checkWatermarks in all the places we're manually checking. Thoughts?
Yes, makes sense.
|
|
||
| void checkHighWatermark(uint32_t bytes_buffered) { | ||
| if (high_watermark_ > 0 && !is_above_high_watermark_ && bytes_buffered > high_watermark_) { | ||
| // Just exceeds high watermark. |
There was a problem hiding this comment.
exceeds -> exceeded the
|
|
||
| void checkLowWatermark(uint32_t bytes_buffered) { | ||
| if (low_watermark_ > 0 && !is_below_low_watermark_ && bytes_buffered < low_watermark_) { | ||
| // Just cross low watermark. |
| // A class, together with a stand alone buffer, used to achieve the purpose of WatermarkBuffer. | ||
| // Itself doesn't have buffer or do bookeeping of buffered bytes. But provided with buffered_bytes, | ||
| // it re-acts upon crossing high/low watermarks. | ||
| class EnvoyQuicSimulatedWatermarkBuffer { |
There was a problem hiding this comment.
unit tests for this please
| bool status_changed{false}; | ||
| if (disable) { | ||
| ++read_disable_counter_; | ||
| ASSERT(read_disable_counter_ == 1); |
There was a problem hiding this comment.
The docs (and Envoy flow control) allows readDisable(true) to be called multiple times. I'd think the second time, read_disable_counter would be 2.
There was a problem hiding this comment.
Sorry, this assert is added for debugging. Removed.
| // TODO(danzh): add interface to quic for connection level buffer throttling. | ||
| // Currently read buffer is capped by connection level flow control. And | ||
| // write buffer is not capped. | ||
| // write buffer limit is set during construction. Change buffer limit during |
There was a problem hiding this comment.
Change -> Changing the
connection -> the connection
wu-bin
left a comment
There was a problem hiding this comment.
Here are some comments so far, I'll continue tomorrow.
| FUNCTION(misc) \ | ||
| FUNCTION(mongo) \ | ||
| FUNCTION(quic) \ | ||
| FUNCTION(quic_stream) \ |
There was a problem hiding this comment.
Why do we need 'quic_stream'? Looking at the list, most dependent libraries only use one logger id.
There was a problem hiding this comment.
EnvoyQuicSimulatedWatermarkBuffer is used in both session and stream, to make the log more clear, I pass in logger instance from its owner. And the logger needs a different id to distinguish each other.
There was a problem hiding this comment.
I see. I'm inclined to replace EnvoyQuicSimulatedWatermarkBuffer.logger_ by EnvoyQuicSimulatedWatermarkBuffer.stream_id, use QuicUtils::GetInvalidStreamId() if it's for a session. That way you can log to the 'quic' logger with a stream id in it.
There was a problem hiding this comment.
EnvoyQuicSimulatedWatermarkBuffer.stream_id is a good idea. That way I can use ENVOY_STREAM_LOG.
But I'm not sure if moving all the quic related log under 'quic' logger is what we want. Quic session and connections are using 'connection' logger currently in parallel to Network::ConnectionImpl
| : quic::QuicServerSessionBase(config, supported_versions, connection.get(), visitor, helper, | ||
| crypto_config, compressed_certs_cache), | ||
| QuicFilterManagerConnectionImpl(std::move(connection), dispatcher) {} | ||
| QuicFilterManagerConnectionImpl(connection.get(), dispatcher, send_buffer_limit), |
There was a problem hiding this comment.
As we talked offline, let's try simplify the lifetimes by changing QuicFilterManagerConnectionImpl to a member of EnvoyQuicServerSession, such that the Impl doesn't need to check if quic_connection_ is nullptr. Feel free to TODO it in a future PR.
There was a problem hiding this comment.
Defer to #8496 so the refactoring can be tested. TODO added.
| #include "quiche/quic/core/quic_session.h" | ||
| #include "quiche/spdy/core/spdy_header_block.h" | ||
|
|
||
| #include "extensions/quic_listeners/quiche/platform/quic_mem_slice_span_impl.h" |
There was a problem hiding this comment.
Could we include platform/api/quic_mem_slice_span.h instead of impl?
There was a problem hiding this comment.
nope, the weird dependency of QuicMemSlice API is app -> impl -> api
| public Http::StreamCallbackHelper, | ||
| protected Logger::Loggable<Logger::Id::quic_stream> { | ||
| public: | ||
| EnvoyQuicStream(uint32_t buffer_limit, std::function<void()> below_low_watermark, |
There was a problem hiding this comment.
Please add a comment for |buffer_limit|.
| // This is counting not serialized bytes in the send buffer. | ||
| uint64_t bytes_to_send_old = BufferedDataBytes(); | ||
| // QUIC stream must take all. | ||
| WriteBodySlices(quic::QuicMemSliceSpan(quic::QuicMemSliceSpanImpl(data)), end_stream); |
There was a problem hiding this comment.
It seems dangerous to ignore the return value of WriteBodySlices. Can we consider the follow behavior instead:
- Use a very large buffered_data_threshold. This threshold has little to do with upstream flow control window etc, it is only used to prevent pathological conditions.
- When we WriteBodySlices, we only consider it success if all data is consumed. Otherwise close the connection.
- After a successful WriteBodySlices, run the watermark callbacks if needed.
| void EnvoyQuicServerStream::resetStream(Http::StreamResetReason /*reason*/) { | ||
| NOT_IMPLEMENTED_GCOVR_EXCL_LINE; | ||
| void EnvoyQuicServerStream::resetStream(Http::StreamResetReason reason) { | ||
| // Higher layers expect calling resetStream() to immediately raise reset callbacks. |
There was a problem hiding this comment.
Higher layers => Upper layers
|
|
||
| void EnvoyQuicServerStream::readDisable(bool /*disable*/) { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } | ||
| void EnvoyQuicServerStream::switchStreamBlockState(bool should_block) { | ||
| ASSERT(FinishedReadingHeaders(), |
There was a problem hiding this comment.
It certainly sounds unlikely, but if we can handle block before finishing headers, we should not ASSERT, a warning log seems not appropriate to me.
Also, can we add some information to the error message? Like the upstream buffer limit and bytes read for headers.
There was a problem hiding this comment.
stream blocking and unblocking can only happen to request body. decodeHeaders() doesn't have buffer limit. The ASSERT here is just for debugging purpose.
| if (should_block) { | ||
| sequencer()->SetBlockedUntilFlush(); | ||
| } else { | ||
| ASSERT(read_disable_counter_ == 0, "readDisable called in btw."); |
There was a problem hiding this comment.
Got it, I thought it is 'by the way'.
wu-bin
left a comment
There was a problem hiding this comment.
I'm still reviewing it. Here are the comments before quic_filter_manager_connection_impl.cc.
| bool empty_payload_with_fin = buffer->length() == 0 && finished_reading; | ||
| if (!empty_payload_with_fin || !end_stream_decoded_) { | ||
| ASSERT(decoder() != nullptr); | ||
| decoder()->decodeData(*buffer, finished_reading); |
There was a problem hiding this comment.
What happens if empty_payload_with_fin==false and end_stream_decoded_==true?
There was a problem hiding this comment.
That's impossible. If fin is already delivered, no new data should arrive.
There was a problem hiding this comment.
Sure. Consider add an assert for that so we can catch it when the assumption is violated.
There was a problem hiding this comment.
I changed the logic here to avoid decoding empty payload with end of stream in case of trailer. ASSERT doesn't apply any more.
| in_decode_data_callstack_ = false; | ||
| if (read_disable_counter_ > 0) { | ||
| // If readDisable() was ever called during decodeData() and it meant to disable | ||
| // reading from downstream, the call must have been deferred. Call it now. |
There was a problem hiding this comment.
Q: Can you elaborate (in this comment) on why the switch call needs to be deferred?
There was a problem hiding this comment.
Commented in EnvoyQuicStream::readDisable() when deferring switch call.
| if (BufferedDataBytes() > 0) { | ||
| // If the stream is closed without sending out all bufferred data, regard | ||
| // them as sent now and adjust connection buffer book keeping. | ||
| dynamic_cast<QuicFilterManagerConnectionImpl*>(session())->adjustBytesToSend( |
There was a problem hiding this comment.
nit: add a filter_manager_connection_impl() function to return the QuicFilterManagerConnectionImpl*. (Feel free to choose a different function name)
| // EnvoyQuicStream | ||
| void switchStreamBlockState(bool should_block) override; | ||
| uint32_t streamId() override; | ||
| Network::Connection* connection() override; |
There was a problem hiding this comment.
nit: How about network_connection(), to distinguish from quic connection.
There was a problem hiding this comment.
This is for ENVOY_CONN_LOG which assumes passed in object has connection() interface.
|
|
||
| // A class, together with a stand alone buffer, used to achieve the purpose of WatermarkBuffer. | ||
| // Itself doesn't have buffer or do bookeeping of buffered bytes. But provided with buffered_bytes, | ||
| // it re-acts upon crossing high/low watermarks. |
There was a problem hiding this comment.
nit: Document the (no-op) behavior when both high and low watermarks are 0.
|
|
||
| void checkHighWatermark(uint32_t bytes_buffered) { | ||
| if (high_watermark_ > 0 && !is_above_high_watermark_ && bytes_buffered > high_watermark_) { | ||
| // This is the first time for the buffer to cross the high watermark |
There was a problem hiding this comment.
nit: How about: // Transitioning from below to above high watermark.
Similar for checkLowWatermark.
| bool is_below_low_watermark_{true}; | ||
| uint32_t high_watermark_{0}; | ||
| bool is_above_high_watermark_{false}; |
There was a problem hiding this comment.
Seems like is_below_low_watermark_ and is_above_high_watermark_ can be replaced by a single last_exceeded_watermark_ which is a tri-state: {NONE, LOW_WATERMARK, HIGH_WATERMARK}. Your call.
There was a problem hiding this comment.
The NONE state seems not necessary. I simplified to two state indicated by a bool.
| } else { | ||
| send_buffer_simulation_.checkLowWatermark(buffered_data_new); | ||
| } | ||
| connection.adjustBytesToSend(buffered_data_new - buffered_data_old); |
There was a problem hiding this comment.
Can we add some code to handle integer overflow/unsigned wraparound? (buffered_data_new - buffered_data_old may not be representable as a int64_t)
There was a problem hiding this comment.
I don't think it's possible to over overflow or underflow as the delta is capped by flow control and congestion control.
There was a problem hiding this comment.
SG. (I agree it's extremely unlikely, I just feel that we can still handle it gracefully even if it happens.)
| return decoder_; | ||
| } | ||
|
|
||
| // True once end of stream is propergated to Envoy. Envoy doesn't expect to be |
| } | ||
|
|
||
| void QuicFilterManagerConnectionImpl::adjustBytesToSend(int64_t delta) { | ||
| bytes_to_send_ += delta; |
There was a problem hiding this comment.
bytes_to_send_ is uint32_t but delta is int64. Can we make bytes_to_send_ 64 bit?
There was a problem hiding this comment.
bytes_to_send_ is used to compare with buffer limits which are all uint32_t. delta is converted from size_t which is very unlikely to overflow given QUIC's flow control and congestion control window.
| // of trying to map it into a 64-bit space. | ||
| stream_info_(dispatcher.timeSource()), id_(quic_connection_->connection_id().Hash()) { | ||
| // TODO(danzh): Use QUIC specific enum value. | ||
| stream_info_(dispatcher.timeSource()), id_(quic_connection_->connection_id().Hash()), |
There was a problem hiding this comment.
Q: How is id_ used? What happens if there is a hash collision?
There was a problem hiding this comment.
Used by Network::Connection::id() which is just for logging and stats as far as I observed.
There was a problem hiding this comment.
SG. Please add a comment for id_ to prevent it from being used as a connection id in the future.
| // Derive to have simpler priority mechanism. | ||
| class TestEnvoyQuicServerSession : public EnvoyQuicServerSession { | ||
| public: | ||
| TestEnvoyQuicServerSession(const quic::QuicConfig& config, |
There was a problem hiding this comment.
nit: You can do using EnvoyQuicServerSession::EnvoyQuicServerSession; to "inherit" constructors. Similar for TestQuicCryptoServerStream.
| quic::ENCRYPTION_FORWARD_SECURE, | ||
| std::make_unique<quic::NullEncrypter>(quic::Perspective::IS_SERVER)); | ||
| // Drive congestion control manually. | ||
| auto send_algorithm = new testing::StrictMock<quic::test::MockSendAlgorithm>; |
There was a problem hiding this comment.
nit: Consider a NiceMock and only do EXPECT_CALL only the interesting calls.(CanSend, GetCongestionWindow, ...)
| decoded_headers->Method()->value().getStringView()); | ||
| })); | ||
| EXPECT_CALL(request_decoder, decodeData(_, true)) | ||
| .Times(testing::AtMost(1)) |
There was a problem hiding this comment.
Since end_stream is already true in decodeHeaders, can we make sure decodeData is not called?
There was a problem hiding this comment.
Please change to .Times(0) for decodeData, or just remove the EXPECT_CALL.
| EXPECT_EQ(Http::Headers::get().MethodValues.Get, | ||
| headers->Method()->value().getStringView()); | ||
| })); | ||
| if (quic_version_.transport_version == quic::QUIC_VERSION_99) { |
There was a problem hiding this comment.
How about if (VersionUsesHttp3(quic_version_.transport_version))?
There was a problem hiding this comment.
QUICHE deps is behind, switch to VersionUsesQpack for now. And it should be updated when next QUICHE update.
| } | ||
| })); | ||
| std::string data = payload; | ||
| if (quic_version_.transport_version == quic::QUIC_VERSION_99) { |
| if (quic_version_.transport_version == quic::QUIC_VERSION_99) { | ||
| std::unique_ptr<char[]> data_buffer; | ||
| quic::HttpEncoder encoder; | ||
| quic::QuicByteCount data_frame_header_length = | ||
| encoder.SerializeDataFrameHeader(payload.length(), &data_buffer); | ||
| quic::QuicStringPiece data_frame_header(data_buffer.get(), data_frame_header_length); | ||
| data = absl::StrCat(data_frame_header, payload); | ||
| } | ||
| quic::QuicStreamFrame frame(stream_id_, fin, 0, data); |
There was a problem hiding this comment.
This pattern is used multiple times in the PR, consider wrap it in a function
quic::QuicStreamFrame MakeQuicStreamFrame(version, stream_id, fin, offset, payload) {
std::string frame_data = VersionUsesHttp3(version) ? frame_header + payload : payload;
return quic::QuicStreamFrame(stream_id, fin, offset, frame_data);
}
There was a problem hiding this comment.
QuicStreamFrame keeps a string view, so it can't be returned. I moved the string manipulating logic into a helper function.
| namespace Envoy { | ||
| namespace Quic { | ||
|
|
||
| class MockEnvoyQuicSession : public quic::QuicSpdySession, public QuicFilterManagerConnectionImpl { |
There was a problem hiding this comment.
Q: if MockEnvoyQuicSession inherits MockQuicSpdySession, can we remove the MOCK_METHODs?
There was a problem hiding this comment.
We don't want to inherit from MockQuicSpdySession because it uses quic::test::DefaultQuicConfig() which sets flow control window to very large value.
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
|
ping? |
| namespace Envoy { | ||
| namespace Quic { | ||
|
|
||
| class MockEnvoyQuicSession : public quic::QuicSpdySession, public QuicFilterManagerConnectionImpl { |
| decoded_headers->Method()->value().getStringView()); | ||
| })); | ||
| EXPECT_CALL(request_decoder, decodeData(_, true)) | ||
| .Times(testing::AtMost(1)) |
There was a problem hiding this comment.
Please change to .Times(0) for decodeData, or just remove the EXPECT_CALL.
| } | ||
|
|
||
| void QuicFilterManagerConnectionImpl::adjustBytesToSend(int64_t delta) { | ||
| bytes_to_send_ += delta; |
| } else { | ||
| send_buffer_simulation_.checkLowWatermark(buffered_data_new); | ||
| } | ||
| connection.adjustBytesToSend(buffered_data_new - buffered_data_old); |
There was a problem hiding this comment.
SG. (I agree it's extremely unlikely, I just feel that we can still handle it gracefully even if it happens.)
|
|
||
| // True after the buffer goes below low watermark and hasn't come up above high | ||
| // watermark yet, even though the buffered data might be between high and low | ||
| // watermarks. |
There was a problem hiding this comment.
Consider add a comment for the behavior when neither high nor low watermarks have been crossed.
There was a problem hiding this comment.
This is the beginning state of the buffer. Comment added.
| // EnvoyQuicStream | ||
| void switchStreamBlockState(bool should_block) override; | ||
| uint32_t streamId() override; | ||
| Network::Connection* connection() override; |
|
|
||
| void EnvoyQuicServerStream::readDisable(bool /*disable*/) { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } | ||
| void EnvoyQuicServerStream::switchStreamBlockState(bool should_block) { | ||
| ASSERT(FinishedReadingHeaders(), |
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
mattklein123
left a comment
There was a problem hiding this comment.
Super awesome to see this coming together. Just flushing out a few small nits/questions from a skim through. Generally LGTM and will defer to @alyssawilk @wu-bin on the detailed review.
/wait
| @@ -0,0 +1,33 @@ | |||
| ### Overview | |||
There was a problem hiding this comment.
nit: can we move to https://github.com/envoyproxy/envoy/tree/master/source/docs? This is where we currently have our dev docs. The top level docs directory is for user docs.
| for (auto& it : quic_server_session_.stream_map()) { | ||
| if (!it.second->is_static()) { | ||
| ENVOY_LOG(debug, "runHighWatermarkCallbacks on stream {}", it.first); | ||
| dynamic_cast<EnvoyQuicServerStream*>(it.second.get())->runHighWatermarkCallbacks(); |
There was a problem hiding this comment.
Why do we need the dynamic cast here and below? Can the stored stream just derive from the right interface?
There was a problem hiding this comment.
stream_map() is a QUICHE interface, for sure it doesn't know about Envoy stream interface.
There was a problem hiding this comment.
I see so this map is storing an opaque pointer of some type? Could it eventually just store a common interface that our classes derive from? Can this be a TODO here and below?
There was a problem hiding this comment.
QuicSession::stream_map() store all QuicStream objects. Our envoy quic stream also derives from that class and from EnvoyQuicStream as well. And the latter has runHighWatermarkCallbacks() interface. So we need a cross cast here I believe.
There was a problem hiding this comment.
I'm happy with the TODO there. Can we have some comment the first time we reference is_static just explaining it's crypto and gQUIC headers?
| // flow control window of upstream. The per-stream high watermark should be | ||
| // smaller than max flow control window to make sure upper stream can be flow | ||
| // control blocked early enough not to send more than the threshold allows. | ||
| SetQuicFlag(FLAGS_quic_buffered_data_threshold, 2 * 16 * 1024 * 1024); // 32MB |
There was a problem hiding this comment.
This seems pretty huge compared to what we typically recommend for HTTP/2 at the edge. Will we eventually make this configurable?
There was a problem hiding this comment.
This threshold needs to be large enough to hold all the data from upstream even after readDisable(). So it should be larger than HTTP2 per stream flow control window whose negotiated value is not accessible at this level. I will add a TODO to plumb through to get that value.
But before that, for QUICHE to work correctly, we need this threshold to be large enough what each encodeData() can consume all the data passed in. This doesn't mean that we are actually buffering that much data because upstream's flow control window probably is not that large after negotiation.
| // the stream to buffer all the data. | ||
| // Ideally this limit should also corelate to peer's receive window | ||
| // but not fully depends on that. | ||
| 16 * 1024, [this]() { runLowWatermarkCallbacks(); }, |
There was a problem hiding this comment.
See my comment above. IMO for QUIC we should start with good edge defaults, since this is really practically only going to be used at the edge. So yeah 256MiB default is way too large. Can we just use different defaults for QUIC and get them right now? If we want to TODO this for discussion during a production readiness pass that is fine with me.
| Network::Connection* EnvoyQuicServerStream::connection() { return filterManagerConnection(); } | ||
|
|
||
| QuicFilterManagerConnectionImpl* EnvoyQuicServerStream::filterManagerConnection() { | ||
| return dynamic_cast<QuicFilterManagerConnectionImpl*>(session()); |
There was a problem hiding this comment.
Same question about the dynamic cast?
There was a problem hiding this comment.
See my previous comment.
| // notified more than once about end of stream. So once this is true, no need | ||
| // to set it in the callback to Envoy stream any more. | ||
| bool end_stream_decoded_{false}; | ||
| int32_t read_disable_counter_{0}; |
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
|
@alyssawilk Comments and TODOs about buffer size are added. PTAL! |
mattklein123
left a comment
There was a problem hiding this comment.
Thanks, LGTM modulo format fix and some nits. Will defer to @alyssawilk @wu-bin for further review.
@danzh2010 can you please open an explicit issue for tuning/hardening QUIC buffer sizes before we call the MVP ready? I want to make sure we are tracking that. Thank you!
|
|
||
| void EnvoyQuicServerStream::encodeHeaders(const Http::HeaderMap& headers, bool end_stream) { | ||
| ENVOY_STREAM_LOG(debug, "encodeHeaders (end_stream={}) {}.", *this, end_stream, headers); | ||
| // QUICHE guarantees to take all the headers. This could cause inifit data to |
| for (auto& it : quic_server_session_.stream_map()) { | ||
| if (!it.second->is_static()) { | ||
| ENVOY_LOG(debug, "runHighWatermarkCallbacks on stream {}", it.first); | ||
| dynamic_cast<EnvoyQuicServerStream*>(it.second.get())->runHighWatermarkCallbacks(); |
There was a problem hiding this comment.
I see so this map is storing an opaque pointer of some type? Could it eventually just store a common interface that our classes derive from? Can this be a TODO here and below?
Signed-off-by: Dan Zhang <danzh@google.com>
|
|
||
| void EnvoyQuicServerStream::encodeHeaders(const Http::HeaderMap& headers, bool end_stream) { | ||
| ENVOY_STREAM_LOG(debug, "encodeHeaders (end_stream={}) {}.", *this, end_stream, headers); | ||
| // QUICHE guarantees to take all the headers. This could cause inifit data to |
| for (auto& it : quic_server_session_.stream_map()) { | ||
| if (!it.second->is_static()) { | ||
| ENVOY_LOG(debug, "runHighWatermarkCallbacks on stream {}", it.first); | ||
| dynamic_cast<EnvoyQuicServerStream*>(it.second.get())->runHighWatermarkCallbacks(); |
There was a problem hiding this comment.
QuicSession::stream_map() store all QuicStream objects. Our envoy quic stream also derives from that class and from EnvoyQuicStream as well. And the latter has runHighWatermarkCallbacks() interface. So we need a cross cast here I believe.
|
@alyssawilk mind take another look? |
alyssawilk
left a comment
There was a problem hiding this comment.
Haha, I was already halfway through! It's looking great - only a few nits left on my end
| runResetCallbacks(reason); | ||
| if (local_end_stream_ && !reading_stopped()) { | ||
| // This is after 200 early response. Reset with QUIC_STREAM_NO_ERROR instead | ||
| // of propagating original reset reason. |
There was a problem hiding this comment.
Can we get a bit more detail here? It isn't obvious how this relates to 200 early response, or how StopReading() translates into Reset(QUIC_STREAM_NO_ERROR)
| // the stream to buffer all the data. | ||
| // Ideally this limit should also corelate to peer's receive window | ||
| // but not fully depends on that. | ||
| 16 * 1024, [this]() { runLowWatermarkCallbacks(); }, |
There was a problem hiding this comment.
Yeah, I thought this one was tied to the upstream limit, i.e. if we set it low we'd end up advertising to the peer we were willing to buffer data when we wouldn't locally. Sounds like I had the wrong field, they can be decoupled and we can start with smaller limits, which SGTM.
| for (auto& it : quic_server_session_.stream_map()) { | ||
| if (!it.second->is_static()) { | ||
| ENVOY_LOG(debug, "runHighWatermarkCallbacks on stream {}", it.first); | ||
| dynamic_cast<EnvoyQuicServerStream*>(it.second.get())->runHighWatermarkCallbacks(); |
There was a problem hiding this comment.
I'm happy with the TODO there. Can we have some comment the first time we reference is_static just explaining it's crypto and gQUIC headers?
|
|
||
| // A class, together with a stand alone buffer, used to achieve the purpose of WatermarkBuffer. | ||
| // Itself doesn't have buffer or bookkeep buffered bytes. But provided with buffered_bytes, | ||
| // it re-acts upon crossing high/low watermarks. |
Commented about QUIC_STREAM_NO_ERROR and static_stream. PTAL! |
Implement encodeHeader|Data|Trailer.
Implement watermark buffer for QUIC stream and session to limit data buffered in stream send buffer.
Implement readDisable() for QUIC stream to block pushing data when upper stream receive buffer is full.
Risk Level: low, not in use
Testing: added tests in stream test.
Part of #2557