CluPilotCloud/app/Livewire/Admin/Capacity.php

113 lines
4.1 KiB
PHP

<?php
namespace App\Livewire\Admin;
use App\Models\Host;
use App\Services\Provisioning\HostCapacity;
use App\Services\Provisioning\ServerMarket;
use Livewire\Attributes\Layout;
use Livewire\Component;
/**
* Who is waiting for a machine, and what machine would clear them.
*
* A paid order that cannot be placed is parked rather than failed — the
* customer has paid, and "no host has room" is a purchasing decision, not a
* fault. This is where that decision is taken: what is queued, what the
* roomiest host still has, what the queue needs, and what such a machine costs
* today.
*
* Nothing here buys anything. Ordering a server is a contract; a bug in a
* capacity calculation must not be able to sign one.
*/
#[Layout('layouts.admin')]
class Capacity extends Component
{
/**
* Look again now.
*
* The runner retries every couple of minutes on its own. This is for the
* operator who has just finished onboarding a host and does not want to
* wait for the tick to see the queue drain.
*/
public function recheck(): void
{
$this->authorize('hosts.manage');
// Through the same query that FINDS them, so "look again" can never
// mean a different set of runs than the table above it is showing.
app(HostCapacity::class)->parkedRuns()->update(['next_attempt_at' => now()]);
$this->dispatch('notify', message: __('capacity.rechecked'));
}
/**
* Decide which parked order goes on which host.
*
* The queue is read by somebody who knows things the placement rule does
* not — that this customer belongs beside that one, that a machine is about
* to be taken out of service, that the big order should have the new box
* and the small ones can share. The rule stays in charge when nobody says
* otherwise; an empty choice hands it back.
*
* Only the preference is written. The reservation still happens in the
* provisioning step, under the placement lock, and still checks the host
* has room — a console that created the instance itself would be a second
* placement path with no lock and its own arithmetic.
*/
public function pin(int $runId, string $hostId): void
{
$this->authorize('hosts.manage');
// Only a run that is actually parked. Writing a host preference onto a
// run that has moved on would sit in its context for ever and take
// effect on a re-run months later, on a host chosen for a queue that no
// longer exists.
$run = app(HostCapacity::class)->parkedRuns()->whereKey($runId)->first();
if ($run === null) {
return;
}
if ($hostId === '') {
$run->forgetContext('preferred_host_id');
$this->dispatch('notify', message: __('capacity.pin_cleared'));
return;
}
$host = Host::query()->where('status', 'active')->find((int) $hostId);
if ($host === null) {
return;
}
$run->mergeContext(['preferred_host_id' => $host->id]);
// Taken up now rather than at the next tick: an operator who has just
// said where it goes is watching for it to go there.
$run->forceFill(['next_attempt_at' => now()])->save();
$this->dispatch('notify', message: __('capacity.pinned', ['host' => $host->name]));
}
public function render()
{
$this->authorize('hosts.manage');
$capacity = app(HostCapacity::class);
$demand = $capacity->queueDemand();
return view('livewire.admin.capacity', [
'parked' => $capacity->parked(),
'demand' => $demand,
'unplaceable' => $capacity->unplaceablePlans(),
'hosts' => Host::query()->where('status', 'active')->orderBy('name')->get(),
// Only when something is actually waiting. A shopping list on an
// idle estate is an invitation to spend money for no reason.
'offers' => $demand['count'] > 0
? app(ServerMarket::class)->offers($demand['largest'])
: [],
]);
}
}