Skip to content

30 July 2026

1 change:

  • CommandBase: one model for long-running workers

CommandBase: one model for long-running workers

Two new Pramnos\Console primitives — SignalStop (cooperative graceful stop) and SystemdNotifier (sd_notify) — join WorkerLock/WorkerReloader, and CommandBase now composes all four into a single long-running-worker model. ProcessQueue and broadcast:serve drop their hand-rolled copies onto it.

See the Console Guide.

Why

The daemon concerns were reimplemented per worker, three different ways:

  • Graceful stop: ProcessQueue trapped only SIGINT (via configureInterruptHandling) and drove a private $shouldContinue; broadcast:serve hand-rolled its own SIGTERM+SIGINT pair; the app's worker.php hand-rolled a third. SIGTERM (systemctl stop / deploy) was not even trapped by ProcessQueue.
  • sd_notify: existed only as a copy-pasted function in an application worker; nothing in the framework spoke the protocol.
  • Heartbeat: CommandBase::heartbeat() took no arguments, so it could not record the rich state (jobs_processed, current_job, …) that WorkerLock::heartbeat() already supports, nor surface the lease-lost signal.

Added (all additive)

  • SignalStop — async SIGTERM/SIGINT handlers that raise a flag (or run an $onStop callback); the loop finishes the current job, then exits. No-op without pcntl.
  • SystemdNotifierready()/watchdog()/stopping()/status() over NOTIFY_SOCKET; every call a no-op off Type=notify. Skips unreachable abstract-namespace sockets.
  • CommandBase gains installStopSignals(?callable), shouldStop() (signals ∪ .stop sentinel), signalStop()/systemd() accessors, a richer heartbeat(array $extra = []): bool (records state, pings the watchdog, returns lease-still-ours), and endJob() now sends systemd STOPPING.

Changed

  • ProcessQueue replaces its $shouldContinue/handleSignal with installStopSignals() + while (!$this->shouldStop()) — now trapping SIGTERM too — with identical stop/lock-missing/max-runtime behaviour.
  • broadcast:serve replaces its inline pcntl pair with installStopSignals(fn () => $server->stop()).

The two documented stop models are now distinct: beginJob()'s SIGINT handler (immediate cleanup+exit) for one-shot lock-guarded commands, and installStopSignals()/shouldStop() (cooperative) for long-running workers.

BC

CommandBase::heartbeat() changed from (): void to (array $extra = []): bool — additive for callers (existing no-arg calls and ignored returns keep working); a subclass that overrides heartbeat() must widen its signature to match. No <namespace>\Application consumer relied on the removed private ProcessQueue members. Full framework suite green, and validated end-to-end against a downstream consumer application.

Tests

SignalStopTest, SystemdNotifierTest (real unix datagram socket), new CommandBaseTest cases (heartbeat $extra + shouldStop signal/sentinel), and the existing ProcessQueueCommandTest/BroadcastServeTest suites (migrated, green).