47 lines
1.8 KiB
PHP
47 lines
1.8 KiB
PHP
<?php
|
|
|
|
namespace App\Console;
|
|
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\Jobs\AdvanceRunJob;
|
|
|
|
/**
|
|
* Scheduler tick (every minute): dispatch an advance job for every run that is
|
|
* due. Immediate dispatch on advance handles the fast path; this catches
|
|
* waiting/retrying runs and anything a crashed worker left behind.
|
|
*
|
|
* PENDING counts as left behind. A run is created and dispatched in two steps,
|
|
* and a process that dies between them leaves a paid customer with a run that
|
|
* nothing will ever pick up. Re-dispatching one that is merely fresh is free —
|
|
* the runner takes a per-run lock and the second job returns immediately.
|
|
*/
|
|
class TickProvisioning
|
|
{
|
|
/**
|
|
* How long a run may sit pending before it counts as stranded.
|
|
*
|
|
* Long enough that an ordinary queue backlog is not mistaken for a crash:
|
|
* sweeping a run whose first job is merely still queued would start a
|
|
* second chain of continuations alongside the first, and both would keep
|
|
* dispatching for the rest of the pipeline.
|
|
*/
|
|
private const STRANDED_AFTER_MINUTES = 5;
|
|
|
|
public function __invoke(): void
|
|
{
|
|
ProvisioningRun::query()
|
|
->where(function ($q) {
|
|
$q
|
|
->whereIn('status', [ProvisioningRun::STATUS_RUNNING, ProvisioningRun::STATUS_WAITING])
|
|
->orWhere(fn ($stranded) => $stranded
|
|
->where('status', ProvisioningRun::STATUS_PENDING)
|
|
->where('created_at', '<=', now()->subMinutes(self::STRANDED_AFTER_MINUTES)));
|
|
})
|
|
->where(function ($q) {
|
|
$q->whereNull('next_attempt_at')->orWhere('next_attempt_at', '<=', now());
|
|
})
|
|
->get()
|
|
->each(fn (ProvisioningRun $run) => AdvanceRunJob::dispatch($run->uuid));
|
|
}
|
|
}
|