161 lines
7.2 KiB
PHP
161 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Jobs;
|
|
|
|
use App\Actions\ArchiveAndTearDown;
|
|
use App\Models\Instance;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
|
|
/**
|
|
* Eine gekündigte Instanz archivieren und abbauen — dort, wo es überhaupt geht.
|
|
*
|
|
* ## Warum ein Auftrag und nicht der Befehl selbst
|
|
*
|
|
* Das ist keine Abwägung, sondern eine Tatsache über diese Installation: nur
|
|
* `queue-provisioning` steigt in den Netz-Namensraum des `vpn-hub` ein
|
|
* (`network_mode: "service:vpn-hub"` in der docker-compose.yml), und dort steht
|
|
* wg0. Der `scheduler`-Container, der `schedule:work` fährt, hat diesen Eintrag
|
|
* nicht. Ein `clupilot:tear-down-ended-instances`, der `ProxmoxClient` selbst
|
|
* aufriefe, hätte also gar keine Route zur Management-Adresse eines Hosts —
|
|
* jeder Abbau endete in einer Zeitüberschreitung, und zwar erst nach der vollen
|
|
* HTTP-Frist. Dieselbe Regel, dieselbe Warteschlange und derselbe Grund wie bei
|
|
* `CollectHostLoad` und `PingHosts` nebenan.
|
|
*
|
|
* Ein Auftrag **je Instanz** und nicht einer für alle: die Warteschlange wird
|
|
* von genau einem Arbeiter seriell abgearbeitet, und ein Abbau, der die Frist
|
|
* reisst, soll die übrigen weder aufhalten noch mitnehmen. Ausserdem ist der
|
|
* einzelne Auftrag die Einheit, die ein Betreiber in `failed_jobs` wiederfindet.
|
|
*
|
|
* ## Die Fristen, und warum sie ineinander stehen müssen
|
|
*
|
|
* `ArchiveAndTearDown` wartet blockierend (`Sleep::for`), mit seinen Vorgaben
|
|
* bis zu 600 + 3600 = 4200 Sekunden je Instanz. Auf einer Warteschlange gibt es
|
|
* darüber zwei weitere Uhren, und wessen Uhr zuerst klingelt, entscheidet, was
|
|
* hinterher dasteht:
|
|
*
|
|
* | Uhr | Wert | was passiert, wenn sie zuerst klingelt |
|
|
* |----------------------------------|--------|----------------------------------------|
|
|
* | die Aktion selbst | 1800 s | wirft, `teardown_error` wird geschrieben, die Konsole zeigt den Grund |
|
|
* | `$timeout` dieses Auftrags | 2100 s | SIGALRM, der Arbeiterprozess stirbt — **kein** Grund am Datensatz |
|
|
* | `retry_after` der Verbindung | 2400 s | der Auftrag gilt als verloren und wird **ein zweites Mal** angefasst |
|
|
*
|
|
* Deshalb bekommt die Aktion hier ausdrücklich kleinere Fristen mitgegeben, als
|
|
* sie selbst vorgibt. Die unterste Uhr ist die einzige, die einen lesbaren
|
|
* Grund hinterlässt; die mittlere hinterlässt einen toten Arbeiter und eine
|
|
* Instanz, die aussieht wie nie angefasst; die oberste startet einen ZWEITEN
|
|
* Abbau gegen eine Maschine, die gerade mitten im vzdump steckt — genau der
|
|
* Fall, den die Prüfung von Aufgabe 3 benannt hat.
|
|
*
|
|
* Die Werte sind Vorgaben, keine Naturgesetze: `PROVISIONING_QUEUE_RETRY_AFTER`
|
|
* lässt sich anheben. Wer das tut, hebt `$timeout` und `BACKUP_WAIT_SECONDS`
|
|
* mit an — in dieser Reihenfolge und mit dem Abstand, den die Tabelle zeigt.
|
|
* `TearDownEndedInstancesTest` nagelt die Staffelung fest, damit eine
|
|
* verschobene Zahl nicht still die Reihenfolge dreht.
|
|
*
|
|
* ## Der Preis dieser Entscheidung, ausgesprochen
|
|
*
|
|
* Zwanzig Minuten fürs Sichern statt der Stunde, die die Aktion allein
|
|
* zuliesse. Eine sehr grosse Kundenplatte kann darin nicht fertig werden. Dann
|
|
* wirft die Aktion mit genau diesem Satz, der Grund steht am Datensatz und in
|
|
* der Konsole, gelöscht wird nichts, und ein Mensch entscheidet — die Fristen
|
|
* anheben oder von Hand archivieren. Das ist der Ausgang, den ein Abbau haben
|
|
* soll: sichtbar stehenbleiben. Ein Auftrag, der stumm vom Arbeiter erschlagen
|
|
* wird, während der vzdump auf dem Knoten weiterläuft, ist es nicht.
|
|
*/
|
|
class TearDownInstance implements ShouldBeUnique, ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
/**
|
|
* Wie lange die Aktion auf das Herunterfahren warten darf.
|
|
*
|
|
* Unverändert die zehn Minuten, die `ShutDownVirtualMachine` demselben
|
|
* Vorgang gibt. Es ist die Frist, in der eine Nextcloud sauber stoppt; sie
|
|
* zu kürzen hiesse, ein Archiv mit halb geschriebenen Tabellen aufzubewahren
|
|
* oder gar nichts.
|
|
*/
|
|
public const SHUTDOWN_WAIT_SECONDS = 600;
|
|
|
|
/**
|
|
* Wie lange die Aktion auf den vzdump warten darf.
|
|
*
|
|
* Zwanzig statt sechzig Minuten, und die Zahl kommt nicht aus einer
|
|
* Schätzung über Plattengrössen, sondern von oben: `$timeout` minus dem
|
|
* Herunterfahren minus dem Zuschlag für Ablagenprüfung, Nachschau und die
|
|
* HTTP-Umläufe dazwischen. Was hier hineinpasst, ist das, was die
|
|
* Warteschlange trägt — siehe die Tabelle im Kopf.
|
|
*/
|
|
public const BACKUP_WAIT_SECONDS = 1200;
|
|
|
|
/**
|
|
* Kein zweiter Versuch.
|
|
*
|
|
* Ein Wiederholungslauf träfe die Maschine an, die der erste Versuch gerade
|
|
* gesichert hat: Proxmox hält eine VM während des vzdump gesperrt, das
|
|
* Herunterfahren schlüge fehl, und der neue, irreführende Grund
|
|
* überschriebe den alten, richtigen. Wiederholt wird hier nicht in
|
|
* Sekunden, sondern in der nächsten Nacht — dann ist der Dump fertig, und
|
|
* die Ausgangslage ist wieder sauber.
|
|
*/
|
|
public int $tries = 1;
|
|
|
|
/**
|
|
* Muss über den Fristen der Aktion und unter `retry_after` liegen.
|
|
*
|
|
* Derselbe Wert wie bei `AdvanceRunJob` und wie `--timeout` im
|
|
* Arbeiterskript, damit hier keine dritte Zahl entsteht, die jemand
|
|
* getrennt pflegen müsste.
|
|
*/
|
|
public int $timeout = 2100;
|
|
|
|
/**
|
|
* Ein Abbau je Instanz, warteschlangenweit.
|
|
*
|
|
* Die Sperre in `ArchiveAndTearDown` hält zwei gleichzeitige LÄUFE
|
|
* auseinander. Sie hält aber keinen zweiten Auftrag davon ab, sich hinter
|
|
* dem ersten in die Warteschlange zu stellen — der liefe dann los, sobald
|
|
* der erste fertig ist, und sicherte dieselbe Maschine ein zweites Mal auf
|
|
* dieselbe Ablage. Das ist kein erfundener Fall: der nächtliche Lauf und
|
|
* ein Betreiber, der den Befehl von Hand anwirft, sind zwei Einreicher.
|
|
*/
|
|
public int $uniqueFor = 2400;
|
|
|
|
public function __construct(public string $uuid)
|
|
{
|
|
$this->onConnection('provisioning');
|
|
$this->onQueue('provisioning');
|
|
}
|
|
|
|
public function uniqueId(): string
|
|
{
|
|
return $this->uuid;
|
|
}
|
|
|
|
public function handle(ArchiveAndTearDown $abbau): void
|
|
{
|
|
$instance = Instance::query()->with('host')->where('uuid', $this->uuid)->first();
|
|
|
|
if ($instance === null) {
|
|
// Zwischen Einreihen und Ausführen können Stunden liegen. Eine
|
|
// Instanz, die es nicht mehr gibt, ist kein Fehlschlag.
|
|
return;
|
|
}
|
|
|
|
$abbau->shutdownWaitSeconds = self::SHUTDOWN_WAIT_SECONDS;
|
|
$abbau->backupWaitSeconds = self::BACKUP_WAIT_SECONDS;
|
|
|
|
// Der Rückgabewert wird bewusst nicht ausgewertet: die Aktion wirft nie
|
|
// und schreibt ihren Grund selbst an den Datensatz, wo die Konsole ihn
|
|
// liest. Ein `false` hier in eine Ausnahme zu übersetzen, machte aus
|
|
// einem vermerkten Fehlschlag zusätzlich einen fehlgeschlagenen
|
|
// Auftrag — dieselbe Nachricht an zwei Orten, von denen nur einer
|
|
// gelesen wird.
|
|
$abbau($instance);
|
|
}
|
|
}
|