Skip to content

Use PeekNamedPipe for Windows pipe read readiness#452

Merged
heartwilltell merged 3 commits into
mainfrom
claude/fix-poller-pipe-read-SpN0Q
May 11, 2026
Merged

Use PeekNamedPipe for Windows pipe read readiness#452
heartwilltell merged 3 commits into
mainfrom
claude/fix-poller-pipe-read-SpN0Q

Conversation

@heartwilltell

Copy link
Copy Markdown
Contributor

The Windows path of run_xev_poll_read submitted a 0-byte ReadFile as a
"readiness probe" via libxev. On IOCP a 0-byte ReadFile completes
synchronously regardless of pipe state, so the parked reader G was woken
immediately — before any data arrived. test_poller_pipe_read_delayed
caught this: the writer thread sleeps 50ms, but the reader had already
checked delayed_writer_done == 0 and stored read_done = -1.

Replace that probe with a PeekNamedPipe-based poll driven by a recurring
5ms libxev timer. The slot bookkeeping (read_g, read_armed) is unchanged;
the timer walks active slots, fires the readiness callback when avail > 0,
and re-arms while any reader is parked. Preserves the readiness-based
public API (callers still issue their own read to consume data) and is
gated to builtin.os.tag == .windows so POSIX paths are untouched. WASI
keeps the empty-buffer-read shape since there is no test coverage and
PeekNamedPipe is unavailable there. Write polling is left untouched —
test_poller_write_park is already #ifndef _WIN32; a follow-up can apply
the same pattern.

claude added 3 commits May 10, 2026 21:03
The Windows path of run_xev_poll_read submitted a 0-byte ReadFile as a
"readiness probe" via libxev. On IOCP a 0-byte ReadFile completes
synchronously regardless of pipe state, so the parked reader G was woken
immediately — before any data arrived. test_poller_pipe_read_delayed
caught this: the writer thread sleeps 50ms, but the reader had already
checked delayed_writer_done == 0 and stored read_done = -1.

Replace that probe with a PeekNamedPipe-based poll driven by a recurring
5ms libxev timer. The slot bookkeeping (read_g, read_armed) is unchanged;
the timer walks active slots, fires the readiness callback when avail > 0,
and re-arms while any reader is parked. Preserves the readiness-based
public API (callers still issue their own read to consume data) and is
gated to builtin.os.tag == .windows so POSIX paths are untouched. WASI
keeps the empty-buffer-read shape since there is no test coverage and
PeekNamedPipe is unavailable there. Write polling is left untouched —
test_poller_write_park is already #ifndef _WIN32; a follow-up can apply
the same pattern.
The Windows PeekNamedPipe poll timer fires every 5ms regardless of
whether any registered pipe has data. With the previous single
loop.run(.once) call inside run_xev_tick_blocking, each spurious timer
fire caused the scheduler's run_poller_poll_blocking to return 0, which
the single-threaded scheduler treats as deadlock — leading to the
"all green threads are deadlocked" abort in
test_poller_pipe_read_delayed.

Loop run_xev_tick_blocking on loop.run(.once) until one of three
conditions holds:
  1. The user-supplied timeout (if any) elapses.
  2. A callback fired that woke a G (callbacks_fired advanced).
  3. The cross-thread async wakeup was signaled (asyncNoop set
     async_fired).

This preserves the existing semantics on Linux/macOS — the first
loop.run(.once) returns on actual I/O readiness or async wake, and
the loop predicate exits immediately — while letting the Windows path
quietly absorb the recurring poll-timer fires until real progress is
made.
@heartwilltell heartwilltell merged commit 169bdde into main May 11, 2026
13 of 14 checks passed
@heartwilltell heartwilltell deleted the claude/fix-poller-pipe-read-SpN0Q branch May 11, 2026 21:34
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