CluPilotCloud/app/Provisioning/Jobs/TearDownInstance.php

182 lines
8.3 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 + 600 = 4800 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
*
* Fünfzehn 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.
*
* Fünfzehn statt sechzig Minuten, und die Zahl kommt nicht aus einer
* Schätzung über Plattengrössen, sondern von oben: `$timeout` minus dem
* Herunterfahren, minus dem Warten auf die Zerstörung, 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.
*
* Waren zwanzig Minuten, bevor das Warten auf `qmdestroy` dazukam. Das
* Sichern gibt die fünf Minuten ab und nicht das Zerstören sie sich, weil
* das Gesamtbudget von 1800 Sekunden die Uhr des Arbeiters unter sich
* halten muss — und weil ein zu kurz geratenes Sichern sichtbar
* stehenbleibt, ein nicht abgewartetes Zerstören dagegen still eine
* laufende Maschine als abgebaut verbuchte.
*/
public const BACKUP_WAIT_SECONDS = 900;
/**
* Wie lange die Aktion auf `qmdestroy` warten darf.
*
* Fünf Minuten. Die Zerstörung ist im Regelfall Sekunden — ein Thin-LVM
* gibt seine Blöcke sofort her —, und was länger braucht, hakt an etwas,
* das ein Mensch ansehen soll: eine VM, die eine fremde Sicherung sperrt,
* ein Volume in Benutzung. Läuft die Frist ab, bleibt die Instanz ohne
* `torn_down_at` stehen, der Grund steht in der Konsole, und die nächste
* Nacht sieht erneut nach.
*/
public const DELETE_WAIT_SECONDS = 300;
/**
* 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;
$abbau->deleteWaitSeconds = self::DELETE_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);
}
}