Skip to content

Workers

Workers

WorkerProcess

A live queue:work subprocess wrapped with spawn metadata.

namespace Cbox\LaravelQueueAutoscale\Workers;

class WorkerProcess
{
    public function __construct(
        public readonly Process $process,
        public readonly string $connection,
        public readonly string $queue,         // singular name OR comma-separated for group workers
        public readonly Carbon $spawnedAt,
        public readonly ?string $group = null,
    ) {}

    public function pid(): ?int;
    public function isRunning(): bool;
    public function isDead(): bool;
    public function isTerminating(): bool;
    public function markTerminationRequested(Carbon $requestedAt, int $timeoutSeconds): void;
    public function terminationDeadlinePassed(Carbon $now): bool;
    public function uptimeSeconds(): int;
    public function matches(string $connection, string $queue): bool;        // false for group workers
    public function matchesGroup(string $connection, string $group): bool;
    public function isGroupWorker(): bool;
    public function getIncrementalOutput(): string;
    public function getIncrementalErrorOutput(): string;
}

WorkerPool

Collection wrapper over WorkerProcess, held in-process by the manager daemon. A web request cannot see it.

namespace Cbox\LaravelQueueAutoscale\Workers;

class WorkerPool
{
    public function add(WorkerProcess $worker): void;
    public function addMany(Collection $workers): void;
    public function removeWorker(WorkerProcess $worker): void;
    public function remove(string $connection, string $queue, int $count): Collection;
    public function removeFromGroup(string $connection, string $group, int $count): Collection;

    public function count(string $connection, string $queue): int;
    public function countGroup(string $connection, string $group): int;
    public function totalCount(): int;          // running and not draining
    public function liveCount(string $connection, string $queue): int;    // running, draining included
    public function liveCountGroup(string $connection, string $group): int;
    public function liveTotalCount(): int;

    /** Running, non-draining workers spawned within the last $seconds (0 or less: none). */
    public function settling(int $seconds): array;   // list<WorkerProcess>
    public function settlingCount(int $seconds): int;

    public function queueCounts(): array;
    public function groupCounts(): array;

    public function all(): Collection;
    public function getDeadWorkers(): Collection;
    public function getTerminatingWorkers(): Collection;
    public function getByConnection(string $connection, string $queue): array;
    public function getTerminatable(string $connection, string $queue, int $count): Collection;
    public function getTerminatableFromGroup(string $connection, string $group, int $count): Collection;
    public function findByPid(int $pid): ?WorkerProcess;
    public function reset(): void;
}

There is no getWorkerCount() — use count($connection, $queue), countGroup(), totalCount(), queueCounts() or groupCounts().

WorkerScaler

Everything that actually changes the pool. The manager decides what the pool should look like; the scaler carries it out, and every method reports what it achieved rather than what it was asked for — the spawner drops workers that fail to launch, and getTerminatable() skips workers already draining.

namespace Cbox\LaravelQueueAutoscale\Workers;

class WorkerScaler
{
    public function scaleUp(ScalingDecision $decision): void;
    public function scaleDown(ScalingDecision $decision): void;
    public function scaleUpGroup(GroupConfiguration $group, ScalingDecision $decision): void;
    public function scaleDownGroup(GroupConfiguration $group, ScalingDecision $decision): void;

    /** Trim a spawn request to the host's remaining `limits.max_total_workers` headroom. */
    public function clampToHostCeiling(int $requested): int;

    public function cleanupDeadWorkers(): void;
    public function enforceTerminationDeadlines(): void;

    /** @return list<string> */
    public function scalingLog(): array;
}

It shares the manager's WorkerPool rather than owning one, so counts read elsewhere in a cycle and counts changed here are always the same pool.

cleanupDeadWorkers() removes every exited worker from the pool and reports each one: a Removed dead worker log line (at info for a graceful exit, warning otherwise) and a WorkerExited event. See Event Handling.

WorkerExitStatus

How a reaped worker process ended. Built by WorkerExitStatus::of(WorkerProcess $worker) from the worker's Symfony Process.

namespace Cbox\LaravelQueueAutoscale\Workers;

readonly class WorkerExitStatus
{
    public function __construct(
        public ?int $exitCode,          // 128 + signal when a signal ended the process
        public ?int $termSignal,        // null when the process exited on its own
        public bool $terminationRequested,
        public int $uptimeSeconds,
    ) {}

    public static function of(WorkerProcess $worker): self;

    /** No signal, and exit code 0 or queue:work's memory-limit code (12). */
    public function isGraceful(): bool;

    /** @return array{exit_code: int|null, term_signal: int|null, termination_requested: bool, uptime_seconds: int} */
    public function toArray(): array;
}

toArray() is the log context of Removed dead worker. A signal the platform cannot report (PHP built with --enable-sigchild) leaves termSignal null rather than throwing.

WorkerMemoryPeakReader

Reads a worker's memory from /proc/<pid>/status, in MB. Both methods return null where /proc is not available or the process is gone.

namespace Cbox\LaravelQueueAutoscale\Workers;

class WorkerMemoryPeakReader
{
    public function __construct(string $procRoot = '/proc') {}

    /** VmHWM: peak resident memory since the process started, boot included. */
    public function peakMb(int $pid): ?float;

    /** VmRSS: resident memory right now. */
    public function residentMb(int $pid): ?float;
}

The manager uses peakMb() for the per-worker peak-memory floor and residentMb() for the memory a booting worker already holds. See Resource Constraints.

WorkerTerminator

namespace Cbox\LaravelQueueAutoscale\Workers;

readonly class WorkerTerminator
{
    /**
     * SIGTERM every worker, wait one shared deadline (the longest shutdown timeout among them),
     * then SIGKILL whatever is left and wait up to a second for it to be reaped.
     *
     * @param  (callable(WorkerProcess): void)|null  $onTerminate  Called per worker as it is signalled.
     * @param  (callable(): void)|null  $whileWaiting  Called about once a second while waiting.
     * @return int Workers that had to be force-killed.
     */
    public function terminateAll(iterable $workers, ?callable $onTerminate = null, ?callable $whileWaiting = null): int;

    public function requestTermination(WorkerProcess $worker): bool;
    public function forceKillIfExpired(WorkerProcess $worker): bool;
    public function terminate(WorkerProcess $worker): bool;
}

terminateAll() marks each signalled worker as termination-requested, so its WorkerExited reports $terminationRequested true. The manager's shutdown passes a $whileWaiting callback that keeps forwarding worker output and reporting exits while workers drain.

WorkerSpawner

Spawns queue:work subprocesses. The command it builds is exactly:

{PHP_BINARY} artisan queue:work {connection} \
    --queue={queue} \
    --tries={workers.tries} \
    --max-time={workers.max_time_seconds} \
    --timeout={workers.timeout_seconds} \
    --sleep={workers.sleep_seconds}

Every value comes from the queue's resolved WorkerConfiguration. --memory is never passed, so queue:work's own default of 128 MB applies: a worker whose PHP memory usage passes it exits with code 12 after its current job and is replaced.

The only environment variables injected into a worker are:

LARAVEL_AUTOSCALE_WORKER=true
AUTOSCALE_MANAGER_ID=<manager id>
AUTOSCALE_WORKER_GROUP=<group name>   # group workers only