Skip to content

162 threadchannel worker parked on recv hangs process shutdown when the sender finishes without close#166

Merged
EdmondDantes merged 6 commits into
mainfrom
162-threadchannel-worker-parked-on-recv-hangs-process-shutdown-when-the-sender-finishes-without-close
Jun 25, 2026
Merged

162 threadchannel worker parked on recv hangs process shutdown when the sender finishes without close#166
EdmondDantes merged 6 commits into
mainfrom
162-threadchannel-worker-parked-on-recv-hangs-process-shutdown-when-the-sender-finishes-without-close

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

No description provided.

…es without close()

A worker spawned with Async\spawn_thread() and parked on ThreadChannel::recv()
kept the whole process alive once the owning side finished without close(): the
non-awaited worker pinned the parent scheduler, so the parent never reached
request shutdown to release the channel, and recv() blocked forever with no
output and no diagnostic.

1. Transparent thread completion event (libuv_reactor.c/.h). The event is hidden
   and its notify handle is unref'd by default, so a non-awaited worker no longer
   keeps the parent loop alive. start() creates the OS thread on the first call
   (ASYNC_THREAD_F_LAUNCHED) and stays transparent; a later start() from an
   awaiter arms the wait (uv_ref + count). stop() disarms back to transparent.
   CLOSED is set on completion in notify_cb after stop(), because the stop
   prologue short-circuits on a closed event and would skip the disarm. An
   awaited worker is kept alive by its suspended coroutine.

2. Endpoint-drop disconnect for PHP ThreadChannels (thread_channel.c/.h). A
   channel left with a single wrapper (ref_count <= 1) has no other thread that
   could be a peer, so parked recv()/send() wake with ThreadChannelException
   ("no producers/consumers remain"), checked both at park (last peer dropped
   before we parked) and on dispose (peer drops while parked). Raw pool channels
   keep auto_disconnect off and close() explicitly.

Tests: thread_channel/042; full thread/thread_channel/thread_pool suites green.

Claude-Session: https://claude.ai/code/session_01NMdPEuD85qzTjv2N4ihTQU
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…el registry

Replaces the ref_count-based disconnect from the previous commit, which was
wrong for fan-out (N>1 workers on one channel) and the thread pool. ref_count
counts wrappers, not endpoints, so no per-channel condition is reliable.

Instead: every live shared channel is tracked in a process-wide registry, and
libuv_reactor_quiesce() closes them all before waiting for child threads. A
worker parked on recv()/send() then wakes with ThreadChannelException and exits.
Combined with the transparent thread event (parent reaches shutdown instead of
pinning on a non-awaited worker), this fixes the hang for any number of workers.

Tests: thread_channel/042 (recv), 043 (producer coroutine), 044 (fan-out).

Claude-Session: https://claude.ai/code/session_01NMdPEuD85qzTjv2N4ihTQU
…ent worker)

Worker output emitted during shutdown disconnect double-flushed on the CI
multi-core runner (a thread-stdout artifact, not a logic double-disconnect;
40x local runs were clean). The tests now keep the worker silent on disconnect
and assert only the owner output: a hang fails via timeout, a spurious value
fails via extra output.

Claude-Session: https://claude.ai/code/session_01NMdPEuD85qzTjv2N4ihTQU
…shutdown

thread_channel/orphan_recv.feature: a coroutine spawns N workers parked on
recv() of a fresh ThreadChannel and finishes without close()/await(). The
shutdown registry close_all() must disconnect every worker. New step in
_harness/Steps.php. 5 .phpt x random:1..30 green on ASAN+async-fuzz (150
interleavings, no hang/leak/UAF), including fan-out.

Claude-Session: https://claude.ai/code/session_01NMdPEuD85qzTjv2N4ihTQU
@EdmondDantes EdmondDantes merged commit 282f122 into main Jun 25, 2026
9 checks passed
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.

ThreadChannel: worker parked on recv() hangs process shutdown when the sender finishes without close()

1 participant