PCNTL Signals in CLI PHP
debt(d8/e3/b3/t7)
Closest to 'silent in production until users hit it' (d8), slightly better than d9 because PHPStan and code patterns matching pcntl_signal/SIGTERM can flag missing dispatch calls, but in practice missing signal handling shows up only when SIGTERM is sent in production and jobs are interrupted mid-execution.
Closest to 'simple parameterised fix' (e3), since the quick_fix is to add pcntl_async_signals(true), register a handler, and check a $running flag in the loop — a small focused change in the worker entry point, not a one-liner but well-contained.
Closest to 'localised tax' (b3), since applies_to is limited to CLI/queue-worker contexts and the signal-handling pattern lives in the worker bootstrap; it doesn't shape the whole codebase but every long-running worker pays the tax.
Closest to 'serious trap' (t7), grounded in misconception that scripts just terminate on SIGTERM and in common_mistakes — forgetting pcntl_signal_dispatch() in loops means handlers silently never fire, and trying to catch SIGKILL is futile; the 'obvious' setup is wrong in non-obvious ways.
TL;DR
Explanation
pcntl_signal(SIGTERM, $handler) registers a callback for OS signals. Signals are checked at tick points (declare(ticks=1)) or via pcntl_signal_dispatch() in loops. Key signals: SIGTERM (graceful shutdown request), SIGINT (Ctrl+C), SIGUSR1/USR2 (custom), SIGHUP (reload config). Pattern for queue workers: set a flag on SIGTERM, check flag in loop, finish current task then exit cleanly. PHP-FPM manages its own signals separately. The pcntl extension is CLI-only — not available in FPM/web context. PHP 8.1: async signals via pcntl_async_signals(true) removes need for ticks.
Common Misconception
Why It Matters
Common Mistakes
- Not calling pcntl_signal_dispatch() in tight loops — signals accumulate but handler never fires.
- Using ticks=1 on every statement — performance overhead. Use pcntl_async_signals(true) instead.
- Handling SIGKILL — impossible to catch, always terminates immediately.
Code Examples
// Worker that can't shut down gracefully:
while (true) {
$job = $queue->pop();
processJob($job); // Killed mid-job on deploy
}
pcntl_async_signals(true); // PHP 8.1+ — no ticks needed
$running = true;
pcntl_signal(SIGTERM, function() use (&$running) {
$running = false;
});
pcntl_signal(SIGINT, function() use (&$running) {
$running = false;
});
while ($running) {
$job = $queue->pop();
if ($job) processJob($job); // Completes current job before exit
else usleep(100000);
}