Repository navigation
Conversation
…waits for credit instead of being reset
This was referenced Oct 17, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #353.
What was wrong.
RunBufferedAsyncwrote a response's DATA bounded by the connection window only._streamsbefore its handler runs, which takes itsPendingRequest.SendWindowout of the write's reach, and any WINDOW_UPDATE for the stream after that point was dropped.What changed. The buffered path now uses the flow control the streamed path already has, instead of its own:
_responseWindowsas soon as its request is ready: inTryComplete, and inBeginStreamedBodywhen request bodies are streamed. It carries over thePendingRequest.SendWindowthe stream had. WINDOW_UPDATE, SETTINGS, RST_STREAM and teardown already maintain that store.TryDispatchStreamedno longer re-seeds the window with_peerInitialStreamWindow. That also stops streamed responses throwing away credit the peer sent while the request was still arriving.WriteDatabounds each frame by the frame size and by both windows. When either window is spent, the rest of the body is copied into a pooledHttp2ResponseWriter. The writer sends it as credit arrives, ends the stream the way a streamed response'sCompleteAsyncdoes, and goes back to the pool. A single internal method,Http2ResponseWriter.ContinueBody, tells the writer the headers have already gone out.Http2Responselets a handler pool or reuse its body once it returns, and the request's own arena is returned straight afterWriteResponse.No collection or counter is added. A parked response lives in
_responseWindows,_creditWaitersand_writerPool, all of which the streamed path already has. A response that fits its windows is still written synchronously, with nothing allocated.Proof.
h2 flow: a buffered response waits on the peer's stream window instead of overrunning it.Http2BufferedCreditTests. This is a sibling file, becauseHttp2FlowControlTestsalready holds 14 tests; it reuses that file's frame helpers, which are nowinternal.h2 flow: a buffered response past the connection window waits for a WINDOW_UPDATE instead of being reseth2 flow: stream credit sent after a request, before its answer, still countsh2 flow: a parked buffered response sends the body it was given, not what its buffer holds laterh2 flow: a parked buffered response stops at the peer's RST_STREAM, and the connection serves onh2 flow: a parked buffered response lets go when the peer sends GOAWAY or closes the connectionRun both ways:
65536 bytes of DATA against a 16384-byte stream window, which the peer may answer with FLOW_CONTROL_ERROR.the response was reset when the connection window ran out, instead of waiting for credit.expected [16384], got [65536].65535 of 102400 bytes: the stream's WINDOW_UPDATE was dropped, and the rest waits on credit the peer already gave.the parked rest was read from the handler's buffer after the handler had handed it back.HandleRstStreamnot waking the stream's waiters. Test 4 fails:the reset left the parked response waiting on credit.Dispose, which undoes http2: a streamed response waiting on credit no longer hangs its handler when the client closes the connection cleanly #333's order. Test 5 fails:the connection ended on a clean close with its parked response still waiting on credit. The streamed path's own test from http2: a streamed response waiting on credit no longer hangs its handler when the client closes the connection cleanly #333 fails too.Suites (main 71aab91 in parentheses):
modprobe tls)ioxide.http2builds for net10.0 and net11.0 with no new warnings. The two CA2014 warnings inHttp2Connection.Write.csare on main too.Bench.
bench/any.sh Http2/ManagedBuffered(h2load, 2 reactors pinned to CPUs 2 and 6, the driver to 0-1 and 12-15). Five rounds alternate main 71aab91, the whole batch merged (for h2, this PR and #378), and main again as the A/A control:Single runs range from 3.83M to 4.37M, so -0.9% is inside the noise. Any real part of it would be the one
_responseWindowsinsert and remove each buffered response now makes.Overlap. #378 (issue #354) changes one condition in
HandleDatain the same file. The two branches merge cleanly (git merge-tree). On the merged tree, Unit is 102 passed and Chaos 80 passed, both with 0 failed and 0 pending.All eight together. This batch's PRs (#376 to #383) were merged onto main 71aab91 in one scratch tree. The only conflict is the binary
libioxide_ngtcp2.sobetween #376 and #377, rebuilt there from the merged shim with the script's defaults. Everything builds, and Unit 102, E2E 309, Http 88, Chaos 80, Tls 169 (14 kTLS cases skipped) and File 5 pass, with 0 failed and 0 pending. The repo's only pending test left is the Redis suite's (#336).Noticed, not changed.
MaxConcurrentStreamsdoes not bound it, because it counts only streams still arriving and not responses being written. That is the known gap to fold into the planned http2 refactor.SendStreamedDatastill has a branch that chargesPendingRequest.SendWindow. With every response window in_responseWindowsit can no longer be reached. It is left for that refactor.