CluPilotCloud/app/Provisioning/WorkInFlight.php

76 lines
3.2 KiB
PHP

<?php
namespace App\Provisioning;
use App\Models\Order;
use App\Models\ProvisioningRun;
use InvalidArgumentException;
/**
* Is the work somebody is about to start already going to happen anyway?
*
* Every customer pipeline shares one subject — the instance's own purchase Order
* — so an action that wants to start a run has to look at what is already in
* flight against it. Two runs writing one router, or resizing one disk from two
* different figures, is the failure this guards.
*
* It used to ask whether ANYTHING was in flight, and stand aside if it was, on
* the reasoning that whatever is running reads the current state when it gets
* there. That is true only of a run that actually contains the steps in question.
* It was false for `restart`, which has no address steps, and for `address`, which
* has no storage steps — with two consequences that both cost the customer
* something real:
*
* - a domain proven while a restart was running was never routed, and nothing
* looked again: the nightly proof check only re-applies an address on the
* FLIP from unproven to proven (see VerifyCustomDomains), and that flip had
* already happened.
* - a storage pack booked while an address run was going was charged every
* month and never delivered, because nothing else ever grows a disk.
*
* So the question is now what it always meant: will the run in flight carry out
* this work? Where the answer is no, the new run starts beside it. That is safe
* for the pairs it allows, and only for those: the caller has already established
* that the machine is LIVE (Instance::hasLiveMachine()), which rules out standing
* beside the build; a run whose pipeline covers the work still wins; and what is
* left writes different things — a router file and a DNS record on one side, a
* disk and a filesystem on the other, with Nextcloud's config.php and its
* appconfig table apart from each other. The one genuinely awkward pair is a
* `restart`, where the guest goes away underneath the newcomer; those steps throw
* on an unreachable guest, which the runner turns into a retry, and the backoff
* outlasts a reboot.
*/
class WorkInFlight
{
public function __construct(private PipelineRegistry $registry) {}
/**
* Will a run already going against this order carry out every step of
* `$pipeline`?
*/
public function covers(Order $order, string $pipeline): bool
{
$runs = ProvisioningRun::query()
->where('subject_type', Order::class)
->where('subject_id', $order->id)
->inFlight()
->get(['pipeline', 'current_step']);
foreach ($runs as $run) {
try {
if ($this->registry->stillCovers($run->pipeline, $run->current_step, $pipeline)) {
return true;
}
} catch (InvalidArgumentException) {
// A pipeline that no longer exists (renamed in a deploy while a
// run was in flight) covers nothing. The run will fail at its
// next advance for the same reason; refusing to start the work
// over it would lose the work as well.
continue;
}
}
return false;
}
}