113 lines
4.1 KiB
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'])
|
|
: [],
|
|
]);
|
|
}
|
|
}
|