Skip to content

fix(http1): fix rare missed write wakeup on connections v2 - #3988

Merged
seanmonstar merged 2 commits into
hyperium:masterfrom
lthiery:poll_loop_v2
Apr 22, 2026
Merged

fix(http1): fix rare missed write wakeup on connections v2#3988
seanmonstar merged 2 commits into
hyperium:masterfrom
lthiery:poll_loop_v2

Conversation

@lthiery

@lthiery lthiery commented Dec 2, 2025

Copy link
Copy Markdown
Contributor

This is a follow up on #3952 which was reverted by #3977 after reports in #3976.

The conceptual problem this is trying to fix is that the dispatch loop may poll the Write side and determine readiness (either rt::Write::poll_write or rt::Write::poll_flush are ready), but will return Poll::Pending to the executor and stall; if neither write nor flush are pending, then nothing will wake us.

The initial fix did not treat the common "unbuffered" implementation where poll_flush always returns Poll::Ready; I question the correctness of such a response, as my understanding of flush would be that the "the bytes are flushed out and we may write again", but I can't argue with what the current convention is.

For testing, in addition to the contrived hyper Stream that triggers the stall from the previous PR (ie: ReadyOnPollStream), I've also added an UnbufferedStream to simulate the implementation where flush always returns Poll::Ready. The test fails as expected with #3952.

The additional fix in this PR is to check again if the connection is writable if and only if we are proceeding to loop because we think the connection may be writable (ie: write or flush have returned Ready). If the write is indeed still pending, we can safely return from the loop without scheduling a waker because we know the writer's waker will schedule us to proceed with the write half. This additional check resolves the hot looping issue for unbuffered streams in #3952.

@seanmonstar
seanmonstar merged commit 743a3ba into hyperium:master Apr 22, 2026
22 checks passed
seanmonstar pushed a commit that referenced this pull request Aug 7, 2026
#4143)

`poll_loop`'s main path always calls `poll_flush` after `poll_write`. The
"wants_write_again" re-check added in #3988 calls `poll_write` a second time
and returns straight out of the loop when it pends, skipping that flush.

That second write can buffer bytes before it pends. When a response body
reaches end-of-stream between the two write polls, `end_body()` buffers the
end of the message and the write then pends on the *next* message
(`poll_msg`). Returning there strands the terminating chunk in the write
buffer: the wake-ups the connection is left waiting on are for reads, so
nothing flushes it. The peer receives the body but never the terminator and
waits until it gives up, at which point the connection reports
`IncompleteMessage` from `mid_message_detect_eof`.

Observed on a server streaming a chunked body fed from another thread, at
roughly one connection in 600k. hyper's own trace shows the divergence:

    healthy:  buf.len=24, buf.len=5, flushed 29 bytes
    stalled:  buf.len=24, flushed 24 bytes, buf.len=5, <nothing>

Flush what the re-check buffered before yielding. Guard the flush on there
being buffered bytes so the call pattern is otherwise unchanged.

Add a test that drives the interleaving deterministically: a body that yields
one data frame, then pends, then ends the stream on the very next poll, all
within a single `poll_loop` iteration.
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.

2 participants