Refund HTTP/2 connection credit for buffered DATA - #1
Conversation
rgarcia
left a comment
There was a problem hiding this comment.
approving — the fix is right and the mechanism checks out. verified the regression test fails on base without the fix, race detector is clean, and the full-suite failure set is byte-identical to master, so the pre-existing breakage claim holds. batching the stream-0 WINDOW_UPDATEs to a half-window cadence instead of per-frame is the right call for the fingerprint too.
one question before we tag a release and bump consumers: the connection window was incidentally the only cap on unread buffered body data per connection (~15.7MB). after this it's 6MB × concurrent streams — we've observed 100+ open unread bodies on a busy host, so worst case is potentially hundreds of MB per connection with nothing bounding the dataBuffer growth. that's what chrome does too, but chrome has its own memory accounting on top and we don't.
can we either:
- size it — what's the p99 concurrent unread bodies per pooled connection under real load? if it's <20 this is a non-issue, or
- add telemetry — a gauge for buffered-unread bytes per ClientConn (sum of bufPipe lengths) exposed so callers can export it, and we watch process RSS during rollout
either answers it. (2) is probably worth having regardless since this class of bug was invisible until we added body-level tracking.
c3db874 to
80ab7d2
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 80ab7d2. Configure here.
| if f.Length > 0 { | ||
| cc.mu.Lock() | ||
| ok := cc.inflow.take(f.Length) | ||
| connAdd := cc.inflow.add(int(f.Length)) |
There was a problem hiding this comment.
Close leaves response body hung
High Severity
transportResponseBody.Close no longer calls bufPipe.BreakWithError, so a Read already blocked on the body never wakes if the peer has not sent END_STREAM. After forgetStreamID, later DATA is dropped rather than written to the pipe, so that Read waits indefinitely. Aborting a slow download via Body.Close now hangs instead of returning errClosedResponseBody.
Triggered by learned rule: HTTP/2 browser windows vs paused-stream starvation
Reviewed by Cursor Bugbot for commit 80ab7d2. Configure here.


summary
unsentConnRefundand are announced once they reach half the connection window, instead of one frame + flush per DATA frame — avoids hot-path write amplification and a per-frame stream-0 WINDOW_UPDATE cadence that no real browser produceswhy this diverges from upstream x/net
upstream returns connection credit on body reads and never hits this starvation because its default connection window is 1 GB against 4 MB stream windows. fhttp advertises browser-realistic windows (
ConnectionFlow15663105 with 6 MB streams for Chrome), so three unread bodies can pin the whole connection window. refunding at buffer time is required here; the batching mirrors the accumulate-and-threshold approach upstream adopted in CL 448155 (inflow).testing
go test -vet=off -run 'TestTransportPausedBodiesDoNotExhaustConnectionWindow' -count=10 ./http2go test -race -vet=off -run 'TestTransportPausedBodiesDoNotExhaustConnectionWindow|TestTransportReturnsUnusedFlowControl|TestTransportReturnsDataPaddingFlowControl' -count=3 ./http2go test -vet=off -run 'TestTransportReturnsUnusedFlowControl|TestTransportReturnsDataPaddingFlowControl|TestTransportAdjustsFlowControl' ./http2TestTransportFlowControland a nil-pointer panic in legacy server tests). verified the failure set is byte-identical between this branch and its base.telemetry accessors
(*ClientConn).BufferedUnreadBytes()and(*Transport).BufferedUnreadBytes(): buffered-but-unread response bytes, for callers to export as a gauge — the connection window no longer bounds this memory, so consumers should watch it during rollout (kernel/kernel#3674 wires it into metro-api).release-lineage note
kernel releases are cut as master +
8ab32de(the stream over-credit fix, pending upstream as bogdanfinn#24) + the module rename. this PR is based on master, and both change the same stream-refresh line intransportResponseBody.Read(and both addtransport_flow_test.go). the correct combined resolution isunsent := int(cc.streamFlow) - int(cs.inflow.n) - cs.bufPipe.Len()— subtract buffered bytes (8ab32de) and use the stream-only window (this PR, sinceavailable()is capped by the batched connection window). branchhypeship/refund-h2-conn-window-rcis that resolution already done: v0.6.8-kernel.1 + this PR merged, all flow tests (includingTestTransportSlowReaderLargeResponse) passing — ready to tag as v0.6.8-kernel.2 once this merges.Note
High Risk
Changes core HTTP/2 client flow-control accounting and WINDOW_UPDATE timing; bugs could starve connections or mis-announce windows, though behavior is covered by targeted regression tests.
Overview
Refunds HTTP/2 connection-level receive window credit when response DATA lands in the stream buffer, instead of when
Response.Bodyis read. Stream-level credit still follows application reads. This prevents multiple paused/unread bodies from pinning the whole connection window under browser-sizedConnectionFlow(~15.6 MiB), which could block other streams.Connection refunds go through new
refundConnFlow: bytes accumulate inunsentConnRefundand are announced with stream-0WINDOW_UPDATEonly after they reach half the connection window, reducing per-frame write churn.transportResponseBody.Closeno longer breaks the pipe early or sends connectionWINDOW_UPDATEfor unread buffered data (avoids double-refund with the buffer-time path).Adds
BufferedUnreadBytes()onClientConnandTransport(plus pool aggregation) so callers can gauge buffered-but-unread response bytes now that the connection window no longer caps that memory.Tests add
TestTransportPausedBodiesDoNotExhaustConnectionWindowand tighten unused-flow-control expectations (full 5000-byte conn refund, no duplicate stream-0 updates).Reviewed by Cursor Bugbot for commit 80ab7d2. Bugbot is set up for automated code reviews on this repo. Configure here.