Skip to content

fix(http2): do not reserve capacity for idle Upgraded streams - #4198

Open
Dev-next-gen wants to merge 1 commit into
hyperium:masterfrom
Dev-next-gen:fix/h2-upgraded-idle-capacity
Open

fix(http2): do not reserve capacity for idle Upgraded streams#4198
Dev-next-gen wants to merge 1 commit into
hyperium:masterfrom
Dev-next-gen:fix/h2-upgraded-idle-capacity

Conversation

@Dev-next-gen

Copy link
Copy Markdown

While reading the fix for #4003 (#4061), I noticed that UpgradedSendStreamTask::tick in src/proto/h2/upgrade.rs still starts every loop with reserve_capacity(1), before it knows whether anything was written to the tunnel. So an idle HTTP/2 CONNECT tunnel pins one byte of the connection window, the same way an idle request body did before #4061.

I added h2_idle_upgraded_does_not_pin_connection_window to tests/client.rs, modelled on the #4061 test: a tunnel writes 65534 bytes and then stays open, against a raw h2 server that never releases capacity. On master the one-byte request B never reaches the server and the test times out after 5 seconds. With this change it passes.

The task now takes the next write from the channel, keeps it in a buffered slot, reserves capacity, waits for it, and then hands the write to h2. That is the same shape as PipeToSendStream. It doesn't pull another write until the buffered one has gone to h2, so the backpressure from #4102 is kept. One difference: since one write sits in the slot instead of the channel, the writer gets one more poll_write accepted before it blocks. Because the task always polls the receiver when it holds nothing, it sees the channel close by itself, so I removed UpgradedCloseNotify, which only served the no-capacity branch.

None of this reaches the public API. Everything touched lives in proto::h2::upgrade, which is pub(crate). UpgradedSendStreamTask does show up in the sealed executor bound in rt/bounds.rs, but its name, generics and auto traits don't change: the removed Arc<UpgradedCloseNotify> and the added Option<Cursor<Box<[u8]>>> are both Send + Sync + Unpin.

cargo test --features full passes, including the CONNECT backpressure tests from #4102, and so does cargo check of the http2 client and server feature combinations with -D dead_code -D unused_imports. My local clippy (1.94) doesn't know a few lints listed in Cargo.toml, so I ran cargo clippy --features full -- -D warnings -A unknown_lints, which reports nothing new.

AI tools used

The send task of an HTTP/2 `Upgraded` stream reserved one byte of capacity at the top of
every loop, before any write was queued. As with `PipeToSendStream` before hyperium#4061, an idle
tunnel then pins the last byte of the connection-level window, and a second stream can
deadlock against peers that only send WINDOW_UPDATE once their window is exhausted.

Pull the next write from the channel first, hold it until h2 has capacity, and only then
reserve. The next write is still not pulled before the held one is handed to h2, so the
writer keeps seeing backpressure. The task now always polls the channel when it holds
nothing, which makes the separate close notification unnecessary, so remove it.

Refs hyperium#4003
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant