Scaling Decision
Scaling Decision
Returned by ScalingEngine::evaluate() and dispatched in ScalingDecisionMade / SlaBreachPredicted events.
namespace Cbox\LaravelQueueAutoscale\Scaling;
readonly class ScalingDecision
{
public function __construct(
public string $connection,
public string $queue,
public int $currentWorkers,
public int $targetWorkers,
public string $reason,
public ?float $predictedPickupTime = null,
public int $slaTarget = 30,
public ?CapacityCalculationResult $capacity = null,
public ?SpawnCompensationConfiguration $spawnCompensation = null,
public ScalingScope $scope = ScalingScope::Host,
public ?SlaSignal $slaSignal = null, // since 4.5
) {}
public function withTargetWorkers(int $targetWorkers, ?string $reason = null): self;
public function shouldScaleUp(): bool;
public function shouldScaleDown(): bool;
public function shouldHold(): bool;
public function workersToAdd(): int;
public function workersToRemove(): int;
public function action(): string; // 'scale_up' | 'scale_down' | 'hold'
public function isSlaBreachRisk(): bool; // predictedPickupTime > slaTarget
}
There is no confidence property on ScalingDecision, or anywhere else in the package.
slaSignal is the SLA signal the engine judged the workload on: what a strategy implementing
SlaSignalStrategy reported, else the oldest job's age. It is null on a decision built outside
the engine — the cluster leader's per-workload decisions and those a policy constructs with new.
withTargetWorkers() carries it over, so a policy that adjusts a target that way keeps it. Breach
state is read from the engine's decision, before policies run, so a policy cannot change it. See
How breach is decided and the
SlaSignal table in Events.
Do not confuse ScalingDecision::action() ('scale_up' | 'scale_down' | 'hold') with WorkersScaled::$action, which is always 'up' or 'down'.
CapacityCalculationResult
ScalingDecision::$capacity, in Cbox\LaravelQueueAutoscale\Scaling\DTOs:
readonly class CapacityCalculationResult
{
public function __construct(
public int $maxWorkersByCpu,
public int $maxWorkersByMemory,
public int $maxWorkersByConfig,
public int $finalMaxWorkers,
public LimitingFactor $limitingFactor,
public array $details = [],
) {}
public function cpuBreakdown(): CpuBreakdown;
public function memoryBreakdown(): MemoryBreakdown;
public function isCpuLimited(): bool;
public function isMemoryLimited(): bool;
public function isConfigLimited(): bool;
public function getSummary(): string;
public function getFormattedDetails(): array;
}
LimitingFactor is a string-backed enum. The values seen on a decision are cpu, memory,
balanced, config, strategy, fuse and system_metrics_unavailable.
The details array
$details is part of the public API: its keys are stable within a major version. It has two
strings and two nested arrays.
| Key | Type | Meaning |
|---|---|---|
cpu_explanation |
string |
For example 85% of 8 cores, current usage: 60.0% |
memory_explanation |
string |
For example 0.6GB available of 2.0GB (cgroup, capped by host free memory), 128MB/worker: headroom, the primary scope's limit, the scope (and the capping scope, if any) and the per-worker estimate |
cpu_details |
array |
See below |
memory_details |
array |
See below |
cpu_details:
| Key | Type | Meaning |
|---|---|---|
max_cpu_percent |
int |
limits.max_cpu_percent |
current_cpu_percent |
float |
CPU usage since the previous tick (50.0 on a process's first tick) |
available_cpu_percent |
int|float |
max(max_cpu_percent - current_cpu_percent, 0) |
total_cores |
float |
Cores available to the process, fractional under a cgroup CPU limit |
reserve_cores |
float |
limits.reserve_cpu_cores |
usable_cores |
float |
max(total_cores - reserve_cores, 0) |
worker_cpu_core_estimate |
float |
Cores one worker is assumed to occupy, at least 0.01 |
cpu_estimate_source |
string |
measured, config or default |
memory_details:
| Key | Type | Since | Meaning |
|---|---|---|---|
max_memory_percent |
int |
limits.max_memory_percent |
|
current_memory_percent |
float |
used_memory_mb / total_memory_mb × 100, for the primary scope |
|
available_memory_percent |
float |
headroom_mb / total_memory_mb × 100 |
|
total_memory_mb |
float |
The primary scope's limit: the one max_memory_percent applies to. Before 4.4 this was always the host |
|
used_memory_mb |
float |
4.4 | Usage measured against that limit. The cgroup working set from 4.4.1 |
headroom_mb |
float |
4.4 | Memory left for new workers after the max_memory_percent ceiling, the cap from every other scope and the settling charge; never negative |
memory_scope |
string |
4.4 | The primary scope: configured, cgroup, host or fallback |
memory_capped_by |
?string |
4.4 | The other scope whose free memory was lower than the primary's headroom, or null |
settling_workers |
int |
4.4 | Workers spawned within limits.worker_settle_seconds and charged the memory they have not grown into yet |
worker_memory_mb |
float |
The per-worker estimate, at least 1.0 | |
memory_estimate_source |
string |
measured, config or default |
|
oom_backoff_ceiling |
?int |
4.4 | The worker ceiling held after an OOM kill, or null when no hold is active |
oom_backoff_until |
?string |
4.4 | When the hold ends, ISO 8601, or null |
The fallback result returned when SystemMetrics::limits() fails has only cpu_explanation,
memory_explanation and error — no nested arrays. The typed accessors return all-default
objects in that case.
CpuBreakdown and MemoryBreakdown
cpuBreakdown() and memoryBreakdown() return the same numbers as cpu_details and
memory_details, as typed readonly objects — prefer them over indexing into the array. Every
constructor argument has a default, so new MemoryBreakdown is a valid "nothing measured" instance
for a stub.
$cpu = $decision->capacity->cpuBreakdown();
$cpu->maxCpuPercent; // float
$cpu->currentCpuPercent; // float
$cpu->availableCpuPercent; // float
$cpu->totalCores; // float
$cpu->reserveCores; // float
$cpu->usableCores; // float, total minus the reserve
$cpu->workerCpuCoreEstimate; // float, cores one worker is assumed to occupy
$cpu->estimateSource; // 'measured', 'config', 'default', or 'unknown' when absent
$memory = $decision->capacity->memoryBreakdown();
$memory->maxMemoryPercent; // float
$memory->currentMemoryPercent; // float, of the primary scope
$memory->availableMemoryPercent; // float, headroom as a percentage of the primary scope's limit
$memory->totalMemoryMb; // float, the limit max_memory_percent applies to
$memory->usedMemoryMb; // float, usage measured against that limit
$memory->headroomMb; // float, room left after the settling charge
$memory->workerMemoryMb; // float
$memory->estimateSource; // 'measured', 'config', 'default', or 'unknown' when absent
$memory->scope; // ?MemoryScopeKind: Configured, Cgroup, Host or Fallback
$memory->cappedBy; // ?MemoryScopeKind, the scope whose free memory was lower
$memory->settlingWorkers; // int, booting workers charged the memory they do not hold yet
$memory->oomBackoffCeiling; // ?int, worker ceiling held after an OOM kill
$memory->oomBackoffUntil; // ?string, ISO 8601 end of that hold
$memory->usedMb(); // float, derived from totalMemoryMb and currentMemoryPercent, 1 decimal
$memory->freeMb(); // float, totalMemoryMb - usedMb(), never negative, 1 decimal
MemoryScopeKind (in the same namespace) is a string-backed enum whose values are the
memory_scope strings. MemoryScopeKind::byPrecedence() lists them most specific first:
Configured, Cgroup, Host, Fallback.
See Resource Constraints for how each value is computed, with worked examples.