Skip to content

fix(pool): dispose per-worker completion futures on shutdown/reload — no leaked cross-thread triggers (#93)#107

Merged
EdmondDantes merged 3 commits into
mainfrom
fix-pool-shutdown-completion-futures
Jul 7, 2026
Merged

fix(pool): dispose per-worker completion futures on shutdown/reload — no leaked cross-thread triggers (#93)#107
EdmondDantes merged 3 commits into
mainfrom
fix-pool-shutdown-completion-futures

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Problem

http_server_start_pool() owns the completion futures returned by pool->submit_internal() (one per worker) but never disposed them. Each future's shared-state cross-thread wakeup trigger (a uv_async on the parent reactor) stayed armed after shutdown:

  • debug: the scheduler's loop-alive assert (scheduler.c — "The event loop must be stopped") aborts on the parent thread's finalize;
  • release: a uv_async handle leaks per worker (async: leftover libuv handle).

reload() leaked the same way, once per rotation.

The reason it was never seen: every existing pool test exits via SIGKILL, so the process dies before the parent reactor finalizes. Async\graceful_shutdown() is the real clean path, and it tripped this.

Fix

Store each worker_evt in pool_worker_ctx_t.done_event and dispose it:

  • in start_pool's cleanup: (covers every exit path, including the gotos);
  • in reload() for the retired cohort, so they don't accumulate across reloads.

Disposal is race-safe against a still-running worker via the paired fix true-async/php-async#182 (remote_future_dispose now severs the producer under the shared-state mutex before closing the trigger). The shared st->cb carries a no-op dispose so freeing a disposed future's callback vector never touches the embedded, shared callback.

Tests

  • 055-pool-graceful-shutdown — pool serves, graceful_shutdown(), start() returns clean.
  • 056-pool-shutdown-timeout-zerosetShutdownTimeout(0), immediate quiesce.
  • 057-pool-reload-then-shutdownreload() then graceful_shutdown(), both cohorts reaped.

All green ×5, no flake (was a race).

Dependency

⚠️ Requires true-async/php-async#182. CI here stays red until that lands in the pinned async build.

…o their cross-thread triggers don't leak on the parent reactor (#93)

http_server_start_pool() owns the completion futures returned by
pool->submit_internal() but never disposed them. Their shared-state cross-thread
wakeup triggers (uv_async on the parent reactor) therefore lingered armed after a
graceful shutdown — tripping the scheduler loop-alive assert on debug builds and
leaking a libuv handle per worker on release. reload() had the same leak per
rotation.

The parent now stores each worker_evt and disposes it: in start_pool's cleanup
(covering every exit path) and in reload() for the retired cohort. Disposal is
race-safe against a still-running worker via the paired
remote_future_dispose fix in php-async (true-async/php-async#182) — the shared
callback carries a no-op dispose so freeing a future's callback vector never
touches the embedded, shared st->cb.

Tests: 055 (graceful_shutdown clean), 056 (setShutdownTimeout(0) immediate
quiesce), 057 (reload then graceful_shutdown — both cohorts reaped).
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Coverage

Total lines: 74.29% → 75.44% (+1.16 pp)

File Baseline Current Δ Touched
src/core/http_connection_tls.c 73.67% 73.39% -0.28 pp
src/http3/http3_callbacks.c 61.64% 61.21% -0.43 pp
src/http3/http3_listener.c 66.35% 66.19% -0.16 pp
src/http_server_class.c 52.02% 61.90% +9.88 pp
src/http_server_config.c 89.83% 90.73% +0.90 pp

)

resume_when(..., true) hands all_done to the waker and waker_clean disposes it,
but the cleanup path disposed it again — a use-after-free in
server_wait_event_dispose (harmless on Linux/debug where the freed slot is still
readable, a hard EXC_BAD_ACCESS crash on macOS ARM64 release). NULL the pointer
after waker_clean so the cleanup dispose is a no-op, mirroring the existing
server->wait_event teardown. Surfaced by the 055-057 clean-shutdown tests — the
only ones reaching this cleanup without SIGKILL.
@EdmondDantes EdmondDantes merged commit 4e2ea28 into main Jul 7, 2026
9 of 10 checks passed
@EdmondDantes EdmondDantes deleted the fix-pool-shutdown-completion-futures branch July 8, 2026 17:55
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