Skip to content

playground: a streaming loop returns once a flush reports the peer gone, so a client that leaves no longer pins its reactor - #407

Merged
MDA2AV merged 1 commit into
mainfrom
fix/404-feed-loops
Oct 26, 2026
Merged

MDA2AV merged 1 commit into
mainfrom
fix/404-feed-loops

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Fixes #404.

The HTTP/2 and HTTP/3 response writers report a peer that has gone (a reset stream or a closed connection) as FlushResult.IsCompleted, and return it synchronously. The samples' /feed loops ignored that result. Once a client left, every flush returned at once, so the loop never yielded and kept the reactor thread forever. No other connection on that reactor was served again.

The fix: every loop that flushes in the six streaming samples returns on IsCompleted:

  • the /feed loops in Http2/ManagedStreamedBoth, Http2/ManagedStreamedResponse, Http2/Nghttp2Response, Http3/ManagedStreamedBoth and Http3/Nghttp3Response;
  • the echo and chunked-download loops in the same handlers, which have the same shape. Http3/Nghttp3StreamedBoth has no /feed, only those.

The docs' panes are generated from the samples, so docs/index.html is regenerated with scripts/gen-docs-panes.py. That covers nine loops: five /feed loops (the four the issue lists, and the HTTP/2 ManagedStreamedBoth one), three echo loops and one chunked download.

Checked

Each /feed sample ran with 2 reactors, pinned. 16 h2load connections streamed /feed for 3 s, then h2load exited. Then: the server's CPU over the next 3 s, and 4 bounded requests (/, or /echo where every other path is /feed).

sample main this PR
Http2/ManagedStreamedBoth 200% CPU, 0 of 4 answered 0%, 4 of 4
Http2/ManagedStreamedResponse 200%, 0 of 4 0%, 4 of 4
Http2/Nghttp2Response 200%, 0 of 4 0%, 4 of 4
Http3/ManagedStreamedBoth 200%, 0 of 4 0%, 4 of 4
Http3/Nghttp3Response 200%, 0 of 4 0%, 4 of 4

Http2/Nghttp2Response was driven with one stream per connection (-m 1). With two endless streams on one nghttp2 connection, the reactor is stuck during the load itself, on main too. That is a separate bug, filed as #409: the second stream's flush returns synchronously while the first one's drain is in flight.

All six samples build. No suite runs them.

Not in this PR: the issue's other direction, a FlushAsync that throws (or completes asynchronously) once it has reported the peer gone, so a handler that forgets the check can't take a reactor down. That changes the writers' behaviour for every consumer, so it's left for its own decision.

Merges cleanly with #406, #393, #386, #384 and #363. Against #307 the only conflict is in HardeningTests.cs, which #307 already has with main.

API: none.

🤖 Generated with Claude Code

…ne, so a client that leaves no longer pins its reactor

The HTTP/2 and HTTP/3 response writers report a peer that has gone - a reset stream or a closed
connection - as FlushResult.IsCompleted, and return it synchronously. The samples' /feed loops
ignored it: once a client left, every flush returned at once and the loop kept the reactor thread
forever, so no other connection on that reactor was served again (#404).

Every loop that flushes in the six streaming samples now returns on IsCompleted: the /feed loops,
and the echo and chunked-download loops in the same handlers, which have the same shape. The docs'
panes are regenerated from the samples with scripts/gen-docs-panes.py.

Checked on each /feed sample with 2 reactors: 16 h2load connections stream /feed for 3 s, then
h2load exits. On main the server stays at 200% CPU and answers none of 4 later requests; with this
change it drops to 0% and answers all 4.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant