Skip to content

fix(coroutine): honour an exception claimed from extended_dispose - #229

Merged
EdmondDantes merged 1 commit into
mainfrom
fix/pool-task-exception-kills-worker
Aug 13, 2026
Merged

fix(coroutine): honour an exception claimed from extended_dispose#229
EdmondDantes merged 1 commit into
mainfrom
fix/pool-task-exception-kills-worker

Conversation

@EdmondDantes

@EdmondDantes EdmondDantes commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Stacked on #228.

A ThreadPool(coroutine: true) task that throws destroys the OS worker thread that ran it, and the pool keeps reporting that worker as alive. With workers: 2: two distinct workers before, one after the first throw, none after the second — while getWorkerCount() answers 2 and isClosed() answers false throughout. With no workers left submit() still accepts and the Futures never settle, so the process hangs with nothing in the log that names the cause.

Cause

Two flags carry near-identical documentation in zend_async_API.h: EXC_CAUGHT (bit 2) and EXCEPTION_HANDLED (bit 6). pool_task_dispose() claims the exception exactly as the API prescribes — it rejects the task's Future and calls ZEND_COROUTINE_SET_EXCEPTION_HANDLED (thread_pool.c:765-767).

The claim is dropped. The sole bit-6-to-bit-2 conversion is at coroutine.c:670-672, immediately after ZEND_ASYNC_CALLBACKS_NOTIFY, and extended_dispose runs at coroutine.c:687-691 — nineteen lines later, after the window has closed. coroutine_object_destroy() gates only on bit 2 (coroutine.c:224), finds no claim and rethrows into the worker's EG(exception). The scheduler's TRY_HANDLE_EXCEPTION at the top of its loop then calls start_graceful_shutdown(), which cancels every coroutine registered on that thread, including the worker's own. The worker leaves its receive loop, the handler returns, and php_request_shutdown() reports the stray exception. The Fatal error line is that shutdown report, not the trigger — there is no bailout anywhere on the path.

Sync-mode pools are unaffected: there the claim is made by zend_async_waker_callback_resolve, which runs inside the notify window.

The change

The conversion is repeated after extended_dispose, so a claim made from that callback counts as well.

The blast radius is small and checkable: ZEND_COROUTINE_SET_EXCEPTION_HANDLED is called on a coroutine in exactly one place in the extension (thread_pool.c:767), and bit 2 is read in exactly one place (coroutine.c:224). Of the five extended_dispose registrants only the pool claims an exception, and it does not want the destructor's rethrow.

Tests

tests/thread_pool/081-coroutine_task_exception_worker_survives.phpt asserts the property that was missing: not that the Future rejects — that always worked — but that the thread which ran the failing task takes the next one. It fails on the base branch and passes with the fix.

The whole suite: 2109 tests, 1857 passed, 248 skipped, 3 failed — the same three pre-existing failures in tests/curl and tests/io, which fail identically without any of this branch's changes.

Left for a separate issue

The pool has no liveness tracking at all: base.worker_count is written only in thread_pool_create and never decremented, and base.workers[] is written and never read. So a worker death cannot be observed by any caller — and reload() sizes its exit-token cohort from that same stale count, which is why it hangs once a worker has gone.

@EdmondDantes
EdmondDantes force-pushed the fix/thread-channel-cancellation-token branch from 1a5116b to 3de312a Compare August 13, 2026 16:44
@EdmondDantes
EdmondDantes force-pushed the fix/pool-task-exception-kills-worker branch from 5c0732a to 0f11b75 Compare August 13, 2026 16:44
@EdmondDantes
EdmondDantes force-pushed the fix/thread-channel-cancellation-token branch from 3de312a to 37f8bb9 Compare August 13, 2026 16:45
@EdmondDantes
EdmondDantes force-pushed the fix/pool-task-exception-kills-worker branch from 0f11b75 to cae984e Compare August 13, 2026 16:45
@EdmondDantes
EdmondDantes force-pushed the fix/thread-channel-cancellation-token branch from 37f8bb9 to 4ab6e98 Compare August 13, 2026 18:02
@EdmondDantes
EdmondDantes force-pushed the fix/pool-task-exception-kills-worker branch from cae984e to 52c6fda Compare August 13, 2026 18:02
@EdmondDantes
EdmondDantes changed the base branch from fix/thread-channel-cancellation-token to main August 13, 2026 18:26
@EdmondDantes
EdmondDantes merged commit a8ebcf4 into main Aug 13, 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