Skip to content

ThreadPool has no liveness tracking: a dead worker is invisible and wedges reload() #231

Description

@EdmondDantes

What is missing

ThreadPool has no liveness tracking, so a worker thread that dies cannot be observed by any caller.

  • getWorkerCount() (thread_pool.c:1764-1769) returns pool->base.worker_count, a plain non-atomic int32_t written only in thread_pool_create (lines 1162, 1194, 1200). Nothing decrements it. It is a construction-time constant.
  • isClosed() reads pool->base.closed, which only explicit close()/cancel() flips. A dying worker never touches it.
  • pool->base.workers[], the OS thread handle array, is write-only: allocated at 1190, written at 901, freed at 1337-1339, and read nowhere in the extension.
  • The worker's own exit path (thread_pool.c:726-729) does a refcount release and frees its context. It records nothing.

So a pool that has lost half its threads still answers with the number it was constructed with, and submit() keeps accepting work whose Futures will never settle.

Where it already bites

reload() takes the cohort size from that same stale count (thread_pool.c:995) and sizes its exit-token channel from it (line 1019), so after a worker death it waits for tokens from threads that no longer exist. Measured: reload() hangs, while the same call on a pool that never lost a worker returns normally.

This was found while chasing #229, where an exception escaping a coroutine-mode task destroyed the worker that ran it. That path is fixed, but the silence around it is not: any future cause of a worker death — a bailout, an OOM, a bug in a bootloader — is equally invisible.

There is a hint that this was noticed once before: tests/thread_pool/070-respawn_worker.php exists in the working tree (gitignored) with a stale .out reading Call to undefined method Async\ThreadPool::respawnWorker().

What would close it

Two separable pieces:

  1. Observation. Mark the slot dead when thread_pool_worker_handler returns, and expose a count of live workers. worker_count would have to become atomic, since it would then be read from PHP-land while workers exit on other threads.
  2. Recovery. Optionally respawn, or at least fail submit() loudly once no worker remains, instead of returning a Future that never settles.

Observation alone is worth having: today no test can express "no worker died", because the value that would say so does not exist. That is precisely why #229 went unnoticed — its tests asserted that the Future rejected, which was always true, while the pool bled threads.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions