Skip to content

fix(thread_pool): graceful-shutdown a retiring sync worker so reload() completes (true-async/server#93)#177

Merged
EdmondDantes merged 1 commit into
mainfrom
93-worker-retire-graceful-shutdown
Jul 4, 2026
Merged

fix(thread_pool): graceful-shutdown a retiring sync worker so reload() completes (true-async/server#93)#177
EdmondDantes merged 1 commit into
mainfrom
93-worker-retire-graceful-shutdown

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

A sync-mode pool worker posts its reload exit-token only after its scheduler drain (RUN_SCHEDULER_AFTER_MAIN) empties. That drain loops while any reactor handle is alive, so a never-ending coroutine the bootloader spawned into the worker's main scope (e.g. an async DB-pool healthcheck timer) keeps the drain — and thus ThreadPool::reload() — hung forever.

On the worker's done: exit, for sync-mode pools only (!coroutine_mode), call start_graceful_shutdown() before the drain: it cancels the main-scope stragglers and arms the drain escape valve, so the worker retires and reload() proceeds. Gated to sync mode — coroutine-mode pools must keep draining their in-flight task coroutines (thread_pool/077).

Also silences a GCC -O2 -Wmaybe-uninitialized false positive across the function's nested zend_try/setjmp blocks with a GCC-only scoped pragma.

Tests: thread_pool 78/79, hot-reload phpt (server) 5/5, zero compiler warnings.

Note: this exposes a separate, pre-existing bug — nested closures (dynamic_func_defs) are shared across worker threads, so a bootloader-spawned nested-closure coroutine can hit a cross-thread run_time_cache use-after-free during rotation. Tracked in #176; not addressed here.

…) completes (true-async/server#93)

A sync-mode pool worker (the HTTP server) posts its reload exit-token only after
its scheduler drain (RUN_SCHEDULER_AFTER_MAIN) empties. The drain loops while any
reactor handle is alive, so a never-ending coroutine the bootloader spawned into
the worker's main scope — e.g. an async DB-pool healthcheck timer — keeps the
drain (and the token, and thus HttpServer::reload()) hung forever.

On the worker's `done:` exit, for sync-mode pools only, call
start_graceful_shutdown() before the drain: it cancels the main-scope stragglers
and arms the drain escape valve, so the worker retires and reload() proceeds.
Gated on !coroutine_mode — coroutine-mode pools must keep draining their in-flight
task coroutines instead (thread_pool/077).

Also silence a GCC -O2 -Wmaybe-uninitialized false positive across the function's
nested zend_try/setjmp blocks (loop-local task pointers + the zend_try macro's own
__orig_bailout) with a GCC-only scoped pragma.

Known follow-up: this exposes an intermittent SIGSEGV during rotation under stress
(a persistent coroutine first-started with a NULL run-time cache) — documented in
the server repo, not yet fixed.
@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes EdmondDantes merged commit b7b8722 into main Jul 4, 2026
9 checks passed
@EdmondDantes EdmondDantes deleted the 93-worker-retire-graceful-shutdown branch July 4, 2026 12:03
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