Skip to content

nghttp3: a streamed response at the send-retention high-water waits for acks instead of spinning - #277

Merged
MDA2AV merged 1 commit into
mainfrom
fix/nghttp3-streamed-at-capacity
Oct 4, 2026
Merged

MDA2AV merged 1 commit into
mainfrom
fix/nghttp3-streamed-at-capacity

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Oct 4, 2026

Copy link
Copy Markdown
Owner

A streamed nghttp3 response whose handler flushes from the dispatch pass (outside the streamed drain) spun once the connection reached the 16 MiB send-retention high-water. There PumpEgress stops taking chunks until acks drain retention, but PumpAsync outside the drain pumped once and returned at once, so FlushAsync asked again - forever, on the reactor thread, which therefore never read the acks that would have ended it. Under h2load (16 conns × 32 streams of 16 × 64 KiB) none of 512 responses completed, and both reactors stayed at 100% CPU after the client had gone.

PumpAsync now parks the writer when the connection cannot queue more, and the capacity signal runs the full drain - which releases parked writers, where PumpEgress alone left them waiting.

Tests

h3: a streamed response past the send-retention high-water completes (new) - 20 MiB in 64 KiB flushes from the dispatch pass:

  • main: FAIL - 0 of 20,971,520 bytes
  • this PR: pass
  • only the park reverted: 0 bytes (the spin); only the drain-on-capacity reverted: stops at exactly 16,777,216 bytes. Both halves are needed.

All suites pass: E2E 232, Unit 60, Chaos 47, Http 44, Tls 151 (the 7 kTLS tests skip without sudo), File 4.

Bench

16 × 64 KiB streamed (Nghttp3Response, 16 conns × 32 streams): main 0 req/s with the reactors spinning; this PR 2,119 req/s.

h2load, 1 KiB, 16 conns × 32 streams, 2 reactors, 4 interleaved rounds with a second build of main as control; within-round median vs main:

sample this PR control
Http3/Nghttp3Response +1.4% +1.9%
Http3/ManagedStreamedBoth -0.8% +0.3%
Http3/ManagedBuffered +0.1% +0.8%
Http3/Nghttp3Buffered -1.4% -2.9%

…or acks instead of spinning

At the high-water PumpEgress stops pulling, so a writer's chunk is not taken until acks
drain retention - and a writer flushing from the dispatch pass looped on PumpAsync, which
outside a drain pumped once and returned at once. The reactor never got back to reading
the acks: 0 of 512 one-MiB responses completed under h2load, both reactors at 100% CPU
for good. PumpAsync now parks it there, and the capacity signal runs the full drain, which
releases parked writers where PumpEgress alone left them waiting.
@MDA2AV
MDA2AV merged commit e0901f2 into main Oct 4, 2026
1 check passed
@MDA2AV MDA2AV mentioned this pull request Oct 4, 2026
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