Repository navigation
engines: HTTP/2 WriteTimeout bounds a response that waits for the client's window, as a minimum rate: stage 2 of the per-stream deadlines (v1.6.0, Refs #889) - #1167
Draft
FumingPower3925 wants to merge 22 commits into
Conversation
…iteTimeout as a minimum rate (red on main) Refs #889
…(D2) A stream that holds buffered bytes for the peer's window releases at least min(64 KiB, what it held at the window's start) per WriteTimeout or is reset with RST_STREAM(INTERNAL_ERROR). Streams starved by the connection window are credited with the conn's releases only if their own window has room. A small per-conn map on the Manager holds the records of stalled streams, ended by every removal path; the fourth hint, WrDL, is read by the sweep. A conn whose stream is stalled is exempt from the conn clocks for WriteTimeout > 0 (A2 retired). Refs #889
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
… append to a stalled stream; frames-to-progress units Refs #889
… into l19d-d2-write-deadline
…arse WINDOW_UPDATEs, a shared slow link, and the bound on a paused stream (red on the previous head) Refs #889
…er and seed, the connection's rate and a bound on a stream below its share, the record judged is the one found due, deadlines that cannot overflow F1: surplus released beyond the floor carries to later windows, up to 4 windows' worth, and a stream's first window is covered by its bytes in flight (the same cap). F6: on a connection that releases at least the floor a stream below its floor is held while it takes half its fair share of the floor, and for 3 consecutive windows if it takes less. F2: the verdict is for the record found due; nothing is reset that has nothing held. F3: timeouts are clamped to a hundred years. Refs #889
…oor-1); the sweep unit with a client of window 0 Refs #889
…end makes no record; the credited cell lasts beyond the idle grace Refs #889
… an unread stream; a silent client's buffer lives for WriteTimeout; the detached-stream paragraph Refs #889
…tep timed out at 540 s with them) Refs #889
…s (it was at 539.2 s of 540 s) Refs #889
… shared are not reset a few bytes short (red: 4 of 12) Refs #889
…he true bounds of an unread stream, the idle count, the share, the seed and the conn floor F-A (r8): the stream's tail, or the connection's, under one floor was judged on all of it in one window; reset 8 to 16 bytes before the end of a shared download. Half of it released is a grace window inside the idle bound. F-C/F-E: the bounds are pinned (an unread stream is reset in window 1+k alone and 1+k+3 with a moving sibling, k = min(4, in flight / 64 KiB)) and eight mutants that survived are killed. Refs #889
…ath.MaxInt64 overflowed now + timeout) Refs #889
…te for parallel downloads and sparse updates, the pool handler's absolute bound Refs #889
Refs #889
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.
Lane L19 H2-TIMEOUTS, stage 2 of the per-stream deadlines: D2, the write deadline of an HTTP/2 stream on epoll, io_uring and adaptive (std unchanged). Refs #889. Built on the merged stage 1 (#1150) per design v3.1 (lane evidence:
design-per-stream-deadlines.md). Draft.What it does
A response whose bytes wait for the client's flow-control window (the tail of an inline handler's body, or a pool handler's, buffered in the stream;
Manager.outboundHeld) had no bound at all (#1134 Finding 1): a client that granted no window and kept pinging held the stream, and its buffer, for ever (measured: 6 s, 60 s, no reset). NowWriteTimeoutbounds it as a minimum rate, #1119's rule at the h2 layer, refined by review r7 so that it does not cut a reader that is slow but moving (final rule and its bounds: design v3.3):WriteTimeoutlongFlushOutbound) at least F =min(64 KiB, what it held when the window began); if so the next window begins from that instant; if not it is reset with RST_STREAM(INTERNAL_ERROR) (net/http's code at a write deadline, and #906's). 64 KiB per window is #1119'sparkedMinAcked: about 64 KiB/s at a 1 s WriteTimeout, about 1 KiB/s at the 60 s defaultF / 2N, N the streams stalled), and for 3 consecutive windows if it took less (paused, dribbled, or queued behind its siblings' bytes in the same socket); the fourth is the verdict. When the connection itself released less than its floor, the first window is the verdict, as for a single stream. So N downloads sharing a slow link are held, and a stream the client does not read pins its buffer for at most 4 x WriteTimeoutWriteTimeout > 0a stalled stream no longer takes the conn out of the conn scan's exemption (A2, #1134: the conn of a silent client was closed at ReadTimeout without a word): the stream is bounded by WriteTimeout, reset, and then the conn is stream-less and its clocks apply (as M2 did for A1). A silent client's unread response, and the buffer it pins, therefore lives for WriteTimeout, not ReadTimeout (the conn used to be closed at ReadTimeout and take the buffer with it). A pool handler blocked inAwaitSendWindowstays counted (its bound is #906's)AwaitSendWindowkeeps its absolute WriteTimeout from the start of the wait; a stream both would reset gets one RST_STREAM (the second finds it cancelled or gone)State: a small per-conn map on the Manager (
stalls, guarded bystallMu, made on the first stall), a record{window start, held, pending, released, conn counters}per stream that holds buffered bytes, and the fourth sweep hintDeadlineHints.WrDL. Nothing inStream(it has 120 free bytes: that is not the reason; the reason is that only a stalled stream, a rare case, needs the state, and the conn credit needs conn-wide state anyway; the per-stream-field alternative is in the design note). Every path that ends a stalled stream ends its record (7 sites, and the sweep's net for a stream gone from the map); the leak test pins each. The common send costs one atomic load ofstallNinReserveSendWindow(to count the conn's releases while any stream is stalled).The RST_STREAM is queued behind the frames the stream already queued (through the conn writer's
Resetter, asresetHandlerStreamdoes): written straight to the connection it could overtake DATA still in the write queue. The bytes the stream held are dropped and refunded to the outbound budget; the stream leaves the conn as a reset from the peer does (a pool handler still running on it retires it); its identifier goes in the reset window, so the client's late WINDOW_UPDATE, RST_STREAM and PRIORITY are ignored (cell).Review r7 (Opus, REQUEST-CHANGES: two MAJOR, three MINOR) and what changed
Tests first (
h2_write_rate_889_linux_test.go), the rule after.curl: (92) HTTP/2 stream 1 was not closed cleanly: INTERNAL_ERROR), complete in 54.5 s now (the review measured main at 54.5 s too); 8 MiB at 1m: complete both. Raw-framer cells (CI-able): a client that updates at half a 1 MiB window reading at 300 KB/s (4.6x the floor): reset at 1.0 s before, complete now; half of 6 MiB at 1 MB/s (gap 3 s): reset at 1.0 s, complete; Go-Transport-like (4 KiB updates): green both. Red on 46947d4: 6 of 9 (the 3 guards green). The fix is the bounded carry-over (4 floors) and the seed; the exact guarantee is in the table. When ReadTimeout <= WriteTimeout (including the 60 s defaults) main closed the whole conn at the same gap, so the new cut was where WriteTimeout < ReadTimeout or ReadTimeout is off.WriteTimeoutnearmath.MaxInt64overflowedarmedAt + wt: every stalled stream had to release 64 KiB per sweep (Go's Transport at 500 KB/s got INTERNAL_ERROR after 8.4 s). Timeouts are clamped to 100 years inSetWriteTimeout,SetRequestTimeoutandSetHeaderBlockTimeout(stage 1's request deadline had the same overflow); tests for all three.connHeld, carries the same bytes; a unit test pins the intended behaviour), and the own-window-shut shelter, killed only by a unit test before, is now also a cell: a paused stream on a healthy conn is reset in the fourth window.UnixNano, the engines' sweepnow, as D1's): a forward step judges every stalled stream at once. Not changed: the sweep'snowis a wall-clock value; documented.Commits (tests first)
3883c56tests, red on maina4d068bthe changewrite_deadline.go,Manager,FlushOutbound,ReserveSendWindow,H2State.Sweep, 7 removal sites; unit tests46947d4)Evidence
internal/protocol/h2/stream,internal/conn): the floor and windows, the carry-over and its cap, the average-rate guarantee (floor on average held for ever, half of it reset once the bank is spent), the seed, the dribbler within two windows, the idle stream on a healthy conn (3 held, 4th reset; its share holds it; a slow conn is the verdict at once), the conn credit, a record ends with every removal path (9 paths), one RST, the re-check, nothing held means no reset, the three clamps, the common path free, a direct send makes no record, frames-to-progress, the sweep end to end.-race -count=5:internal/conn,internal/tcpack,internal/protocol/...ok; the new and changed cells ok (752 s);internal/engine/epoll(381 s) andinternal/engine/iouring(492 s) under-raceok.test/serverH2|889|Shutdown|SSE|Streamset, non-race: 1674 PASS, 0 FAIL, and again on the head merged with main after epoll, io_uring: every HTTP/1 response after Shutdown carries Connection: close and ends its conn (v1.6.0, Refs #885 #884) #1169 (Connection: close after Shutdown) with the1165|884cells added: 1704 PASS, 0 FAIL.reststep (-race,-timeout=540s) time out; the 9 slow cells are listed in.github/root-test-shards.txt(shards a and b,root-shards.sh checkok) so that the rest stays inside its timeout. CI at the merged head56ca76a: 29 pass, 2 skipping.-tags netem889);GOOS=windowsanddarwingo vetok;apidumpok; size tests pass (connState 448,H2State648 bytes = the 704 malloc class,Streamunchanged).test/serverpackage, no privileges, so CI runs them all. The curl numbers above are a one-off measurement (docker, curl 8.14.1/nghttp2 1.64.0), not a cell: a CI cell that skips without curl would trip the skip rule.Mutants (each applied, the named tests run, file restored;
logs/mutant-d2-*.log)PoolDirect)ReserveSendWindowheldconnHeld) carries the same bytes, so the verdict is the same; the unit test pins the intended behaviourHot path and cost
Docker arm64
--cpus 2(a shared machine; amd64 unmeasured), main (3f8d83f) vs head, interleaved, 5 rounds x 2 runs per engine, medians (minimums): the sweep is where D2 adds a per-conn cost (one more atomic, hints 8 bytes longer):(The review measured +6.6 to +7.9% at 4096 on epoll: the same order.) At 10 000 h2 conns and 40 sweeps a second the addition is about 0.2 ms of a second. The conn and stream benchmarks (
ProcessH2Post889,StreamedResponse889,OneByteGrant911) are within the noise of the shared machine, allocations unchanged (previous round:ProcessH2Post889/single673 vs 668 ns,StreamedResponse889/closed559 vs 548 ns; the review: +2 to 3% medians, minimums equal or lower). The common send pays one atomic load ofstallNinReserveSendWindow. A stalled stream's flush takes the books' lock once:BenchmarkOneByteGrantWriteDeadline889on 61 to 73 ns, off 53 to 122 ns (noise-bound). The window verdict allocates when more than 32 streams of a conn are due at once.Release notes (draft)
WriteTimeout: -1nothing changes.WriteTimeoutorReadTimeoutofmath.MaxInt64no longer makes every deadline due at once (timeouts are clamped to 100 years).Findings, not fixed here
AwaitSendWindowis bounded by fix(h2): bound the response bytes a connection holds for the peer's window; over the budget, handlers run on the pool and send as the windows open (celeris#893) #906's absolute WriteTimeout, a buffered tail by this rate: the two policies differ. Aligning them is a follow-up (design v3.7 Q1).now): a forward clock step judges every stalled stream at once, as D1's does (r7 F5).ExpiredWritesallocates when more than 32 streams of one conn are due at once. The per-expiry budget (2 ms per conn) is the stage 1 one.Unverified
internal/protocol/h2/stream, engine-independent), and the new cells ran here on all three engines.