-
-
Notifications
You must be signed in to change notification settings - Fork 36.6k
quic: apply multiple fixes to flow control signaling #65309
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -1083,6 +1083,14 @@ class Http3ApplicationImpl final : public Session::Application { | |
| if (auto stream = session->FindStream(id)) { | ||
| return stream; | ||
| } | ||
| // A locally-initiated stream can only exist because we created it, so if | ||
| // we have no record of it the application already destroyed it. Frames the | ||
| // peer had already put in flight must not bring it back to life -- see | ||
| // DefaultApplication::ReceiveStreamData for the same guard on the raw | ||
| // QUIC path. | ||
| if (!session->is_destroyed() && ngtcp2_conn_is_local_stream(*session, id)) { | ||
| return {}; | ||
| } | ||
| if (auto stream = session->CreateStream(id)) { | ||
| return stream; | ||
| } | ||
|
|
@@ -1224,6 +1232,31 @@ class Http3ApplicationImpl final : public Session::Application { | |
| return NGHTTP3_ERR_CALLBACK_FAILURE; | ||
| } | ||
| auto& session = app.session(); | ||
|
|
||
| // If the application destroyed a request stream it initiated, DATA frames | ||
| // the peer had already sent can still arrive. Ignore that payload rather | ||
| // than resurrecting the stream or tearing down the connection, but return | ||
| // its connection-level flow control credit: nghttp3 hands DATA payload to | ||
| // us uncredited (it is excluded from the framing bytes credited by the | ||
| // caller), so dropping it silently would permanently shrink the session's | ||
| // shared receive window. | ||
| // The is_destroyed() check has to come first: an earlier nghttp3 callback | ||
| // in this same batch may have destroyed the session (for example because a | ||
| // JS callback threw), and neither the ngtcp2 connection nor the flow | ||
| // control helpers below may be touched afterwards. | ||
|
Member
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: a lot of these really large comments are saying exactly the same thing in detail in many places, and with a lot of words. I think a brief comment here is useful, but this is all a bit much. |
||
| if (!session.is_destroyed() && !session.FindStream(id) && | ||
| ngtcp2_conn_is_local_stream(session, id)) { | ||
| Debug(&session, | ||
| "HTTP/3 discarding %zu bytes for destroyed local stream %" PRIi64, | ||
| datalen, | ||
| id); | ||
| if (datalen > 0) { | ||
| Session::SendPendingDataScope send_scope(&session); | ||
| session.ExtendOffset(datalen); | ||
| } | ||
| return NGTCP2_SUCCESS; | ||
| } | ||
|
|
||
| if (auto stream = FindOrCreateStream(conn, &session, id)) [[likely]] { | ||
| stream->ReceiveData(data, datalen, Stream::ReceiveDataFlags{}); | ||
| return NGTCP2_SUCCESS; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1499,12 +1499,33 @@ void Stream::EndWriting() { | |
| if (!is_pending()) session_->ResumeStream(id()); | ||
| } | ||
|
|
||
| void Stream::ReturnFlowControlCredit(uint64_t amount, CreditScope scope) { | ||
| if (amount == 0) return; | ||
| // The stream may outlive a destroyed session (the JS side can still hold a | ||
| // reader over the inbound queue), in which case there is no window left to | ||
| // extend. | ||
| if (!session_ || session_->is_destroyed()) return; | ||
| // Extending a window queues MAX_STREAM_DATA / MAX_DATA frames. The scope | ||
| // ensures they get flushed to the peer. When we are inside an ngtcp2 | ||
| // callback the flush is a no-op (can_send_packets() is false) and the | ||
| // frames go out with the next scheduled send instead. | ||
| Session::SendPendingDataScope send_scope(&session()); | ||
| if (scope == CreditScope::STREAM_AND_CONNECTION && !is_pending()) { | ||
|
Member
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. This |
||
| session().Consume(id(), amount); | ||
| } else { | ||
| session().ExtendOffset(amount); | ||
| } | ||
| } | ||
|
|
||
| void Stream::CreditConsumedBytes(uint64_t amount) { | ||
| uncredited_bytes_ -= std::min(uncredited_bytes_, amount); | ||
| ReturnFlowControlCredit(amount, CreditScope::STREAM_AND_CONNECTION); | ||
|
Member
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. This guards with |
||
| } | ||
|
|
||
| void Stream::EntryRead(size_t amount) { | ||
| // Called when the JS consumer reads data from the inbound DataQueue. | ||
| // Extend the flow control window so the sender can transmit more. | ||
| if (session().is_destroyed()) return; | ||
| Session::SendPendingDataScope send_scope(&session()); | ||
| session().Consume(id(), amount); | ||
| CreditConsumedBytes(amount); | ||
| } | ||
|
|
||
| void Stream::BeforePull() { | ||
|
|
@@ -1517,16 +1538,23 @@ void Stream::BeforePull() { | |
|
|
||
| void Stream::FlushAccumulation() { | ||
| if (!recv_accumulator_ || recv_accumulator_->available() == 0) return; | ||
| size_t flushed = recv_accumulator_->available(); | ||
| auto entry = recv_accumulator_->Flush(env()); | ||
| if (entry) { | ||
| inbound_->append(std::move(entry)); | ||
| // Flush() always drains the accumulator, so the stat is reset either way. | ||
| STAT_SET(Stats, bytes_accumulated, 0); | ||
| if (entry && inbound_->append(std::move(entry)).value_or(false)) { | ||
| // Notify the reader that data is now available in the DataQueue. | ||
| // This is the only place we notify — not on every ReceiveData call — | ||
| // so the reader only wakes up when there is a well-sized entry to | ||
| // consume. | ||
| if (reader_) reader_->NotifyPull(); | ||
| return; | ||
| } | ||
| STAT_SET(Stats, bytes_accumulated, 0); | ||
| // The bytes did not make it into the queue (it is capped and this data | ||
| // would push it past the final size), so they will never reach a reader | ||
| // and EntryRead() will never fire for them. Return their credit here | ||
| // instead of leaking it. | ||
|
Member
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. This one also looks like it should never happen. Fixing the credit is good, but we should at least log it, probably DCHECK it. AFAICT reaching this implies the inbound data was capped (EndReadable) but then more data arrived afterwards anyway (somehow, but can't be the same batch - EndReadable flushes before capping). Ngtcp2 should stop that ever happening I think. If it ever did happen, this branch would imply silent data loss since we're just dropping inbound stream data. |
||
| CreditConsumedBytes(flushed); | ||
| } | ||
|
|
||
| int Stream::DoPull(bob::Next<ngtcp2_vec> next, | ||
|
|
@@ -1652,6 +1680,16 @@ void Stream::Destroy(QuicError error) { | |
| // the ring buffer memory. | ||
| recv_accumulator_.reset(); | ||
|
|
||
| // Any data that was received but never consumed is still holding inbound | ||
| // flow control credit. Once the backpressure listener is detached below, | ||
| // EntryRead() will never fire for it again, so return that credit now. | ||
| // The stream-level window is irrelevant at this point (the stream is going | ||
| // away) but the connection-level window is shared by the whole session: | ||
| // leaking it here would permanently shrink the session's receive window | ||
| // and, over enough streams, deadlock the connection. | ||
| ReturnFlowControlCredit(uncredited_bytes_, CreditScope::CONNECTION_ONLY); | ||
| uncredited_bytes_ = 0; | ||
|
|
||
| // We reset the inbound here also. However, it's important to note that | ||
| // the JavaScript side could still have a reader on the inbound DataQueue, | ||
| // which may keep that data alive a bit longer. | ||
|
|
@@ -1691,6 +1729,15 @@ void Stream::ReceiveData(const uint8_t* data, | |
| Debug(this, "Receiving %zu bytes of data", len); | ||
| if (state()->read_ended == 1 || len == 0) { | ||
| if (flags.fin) EndReadable(); | ||
| // These bytes are being discarded, but ngtcp2 already charged them | ||
| // against both receive windows when it delivered them to us. Nothing | ||
| // downstream will ever consume them, so give the credit back now. | ||
| // This is reachable, for instance, when HTTP/3 replays DATA payload | ||
| // that it had buffered for QPACK head-of-line blocking after the | ||
| // readable side was already shut down. | ||
| if (len > 0) { | ||
| ReturnFlowControlCredit(len, CreditScope::STREAM_AND_CONNECTION); | ||
|
Member
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. Should this be just |
||
| } | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -1699,6 +1746,11 @@ void Stream::ReceiveData(const uint8_t* data, | |
| STAT_SET(Stats, max_offset_received, STAT_GET(Stats, bytes_received)); | ||
| STAT_RECORD_TIMESTAMP(Stats, received_at); | ||
|
|
||
| // These bytes now hold inbound flow control credit. The credit is returned | ||
| // incrementally as the JS consumer reads them (EntryRead), and any | ||
| // remainder is returned when the stream is destroyed. | ||
| uncredited_bytes_ += len; | ||
|
|
||
| // Lazy-allocate the receive accumulation buffer on first data-carrying | ||
| // call. Streams that never receive data (write-only, immediately reset) | ||
| // pay zero cost. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.