-
Notifications
You must be signed in to change notification settings - Fork 127
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
fix(http3): only add to stream_has_pending_data on has_data_to_send #1793
Conversation
`<SendMessage as Sendstream>::send_data` attempts to send a slice of data down into the QUIC layer, more specifically `neqo_transport::Connection::stream_send_atomic`. While it attempts to send any existing buffered data at the http3 layer first, it does not itself fill the http3 layer buffer, but instead only sends data, if the lower QUIC layer has capacity, i.e. only if it can send the data down to the QUIC layer right away. https://github.com/mozilla/neqo/blob/5dfe106669ccb695187511305c21b8e8a8775e91/neqo-http3/src/send_message.rs#L168-L221 `<SendMessage as Sendstream>::send_data` is called via `Http3ServerHandler::send_data`. The wrapper first marks the stream as `stream_has_pending_data`, marks itself as `needs_processing` and then calls down into `<SendMessage as Sendstream>::send_data`. https://github.com/mozilla/neqo/blob/5dfe106669ccb695187511305c21b8e8a8775e91/neqo-http3/src/connection_server.rs#L51-L74 Thus the latter always marks the former as `stream_has_pending_data` even though the former never writes into the buffer and thus might actually not have pending data. Why is this problematic? 1. Say that the buffer of the `BufferedStream` of `SendMessage` is empty. 2. Say that the user attempts to write data via `Http3ServerHandler::send_data`. Despite not filling the http3 layer buffer, the stream is marked as `stream_has_pending_data`. 3. Say that next the user calls `Http3Server::process`, which will call `Http3Server::process_http3`, which will call `Http3ServerHandler::process_http3`, which will call `Http3Connection::process_sending`, which will call `Http3Connection::send_non_control_streams`. `Http3Connection::send_non_control_streams` will attempt to flush all http3 layer buffers of streams marked via `stream_has_pending_data`, e.g. the stream from step (2). Thus it will call `<SendMessage as SendStream>::send` (note `send` and not the previous `send_data`). This function will attempt the stream's http3 layer buffer. In the case where the http3 layer stream buffer is empty, it will enqueue a `DataWritable` event for the user. Given that the buffer of our stream is empty (see (1)) such `DataWritable` event is always emitted. https://github.com/mozilla/neqo/blob/5dfe106669ccb695187511305c21b8e8a8775e91/neqo-http3/src/send_message.rs#L236-L264 The user, on receiving the `DataWritable` event will attempt to write to it via `Http3ServerHandler::send_data`, back to step (2), thus closing the busy loop. How to break the loop? This commit adds an additional check to the `stream_has_pending_data` function to ensure it indeed does have pending data. This breaks the above busy loop. In addition, it renames the function to `stream_might_have_pending_data`.
Benchmark resultsPerformance differences relative to a65d945.
Client/server transfer resultsTransfer of 134217728 bytes over loopback.
|
Thanks @martinthomson for the review. Would you mind taking another look? |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #1793 +/- ##
=======================================
Coverage 93.12% 93.12%
=======================================
Files 116 116
Lines 36097 36100 +3
=======================================
+ Hits 33614 33617 +3
Misses 2483 2483 ☔ View full report in Codecov by Sentry. |
<SendMessage as Sendstream>::send_data
attempts to send a slice of data down into the QUIC layer, more specificallyneqo_transport::Connection::stream_send_atomic
.While it attempts to send any existing buffered data at the http3 layer first, it does not itself fill the http3 layer buffer, but instead only sends data, if the lower QUIC layer has capacity, i.e. only if it can send the data down to the QUIC layer right away.
neqo/neqo-http3/src/send_message.rs
Lines 168 to 221 in 5dfe106
<SendMessage as Sendstream>::send_data
is called viaHttp3ServerHandler::send_data
. The wrapper first marks the stream asstream_has_pending_data
, marks itself asneeds_processing
and then calls down into<SendMessage as Sendstream>::send_data
.neqo/neqo-http3/src/connection_server.rs
Lines 51 to 74 in 5dfe106
Thus the latter always marks the former as
stream_has_pending_data
even though the former never writes into the buffer and thus might actually not have pending data.Why is this problematic?
Say that the buffer of the
BufferedStream
ofSendMessage
is empty.Say that the user attempts to write data via
Http3ServerHandler::send_data
. Despite not filling the http3 layer buffer, the stream is marked asstream_has_pending_data
.Say that next the user calls
Http3Server::process
, which will callHttp3Server::process_http3
, which will callHttp3ServerHandler::process_http3
, which will callHttp3Connection::process_sending
, which will callHttp3Connection::send_non_control_streams
.Http3Connection::send_non_control_streams
will attempt to flush all http3 layer buffers of streams marked viastream_has_pending_data
, e.g. the stream from step (2). Thus it will call<SendMessage as SendStream>::send
(notesend
and not the previoussend_data
). This function will attempt the stream's http3 layer buffer. In the case where the http3 layer stream buffer is empty, it will enqueue aDataWritable
event for the user. Given that the buffer of our stream is empty (see (1)) suchDataWritable
event is always emitted.neqo/neqo-http3/src/send_message.rs
Lines 236 to 264 in 5dfe106
The user, on receiving the
DataWritable
event will attempt to write once more to the stream viaHttp3ServerHandler::send_data
, back to step (2), thus closing the busy loop.How to break the loop?
This commit adds an additional check to the
stream_has_pending_data
function to ensure it indeed does have pending data. This breaks the above busy loop. In addition, it renames the function tostream_might_have_pending_data
.This e.g. surfaces in the 1-conn/10_000-1b-seq-resp (aka. RPS) benchmark.
neqo/neqo-bin/benches/main.rs
Lines 39 to 44 in 5dfe106