Fix-Runde 3: Wiederholbarkeit ehrlich gemacht, Seeder-Zaehler darf nicht sinken
Zwei Befunde, die die letzte Fix-Runde selbst eingefuehrt hat:
- Der Kommentar ueber dem dns_name-Block behauptete, ein Fehlschlag am Riegel
liesse next_host_number schon da, dns_name aber noch stehen - verkehrt
herum, dropColumn laeuft VOR dem Riegel. Scheitert also ausgerechnet
unique('name'), ist dns_name schon weg, und ein zweiter Anlauf starb an der
Vorabpruefung mit "Unknown column 'dns_name'" statt an der Stelle, die der
Kommentar nannte. $hadDnsName wird jetzt einmal ermittelt und bindet
Vorabpruefung, Uebertragung und den Spalten-Block an dasselbe Urteil -
faellt dns_name schon in einem frueheren Anlauf, prueft die Vorabpruefung
direkt auf name statt auf den nicht mehr vorhandenen CASE-Ausdruck, und
meldet Doppelte weiterhin sauber statt sie stillschweigend durchzulassen.
Kommentar korrigiert; die verbleibende (sehr schmale) Grenze - gelingt
unique('name'), scheitert nur noch die DELETE-Zeile danach - ehrlich als
offen benannt statt verschwiegen oder ungeprueft behauptet zu sein.
- DatabaseSeeder schrieb next_host_number=2 durch den Update-Teil von
updateOrCreate und drehte damit den Zaehler bei jedem Re-Seed einer
Installation zurueck, die laengst weiterzaehlte - genau die Wieder-
verwendung, gegen die dieser Umbau gebaut wurde. Jetzt max(vorhanden, 2):
frisch angelegt hebt es auf 2, bestand die Zeile schon und zaehlte hoeher,
bleibt sie stehen.
Beide Fixe gegen echtes MariaDB auf eigenen Wegwerf-Datenbanken geprueft
(drei Migrationslaeufe fuer den ersten Befund, zwei Saatlaeufe fuer den
zweiten), nicht auf der geteilten Entwicklungsdatenbank. Gezielte Testlaeufe
(173 + 21 bestanden) statt der vollen Suite, wie vorgegeben. Bericht
angehaengt an .superpowers/sdd/2026-08-01-hostname-vergabe/final-fix-report.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/neue-pakete
parent
c3c05ff9f8
commit
e643769be2
|
|
@ -31,6 +31,16 @@ return new class extends Migration
|
|||
{
|
||||
public function up(): void
|
||||
{
|
||||
// EINMAL geprüft, für den ganzen Lauf festgehalten. `dns_name` wird
|
||||
// weiter unten an drei Stellen angefasst — gelesen (Vorabprüfung),
|
||||
// geschrieben (Übertragung) und gelöscht (Spalten-Block) — und alle
|
||||
// drei müssen auf demselben Stand urteilen. Ein zweiter Anlauf nach
|
||||
// einem Fehlschlag, der die Spalte schon fallen ließ (der
|
||||
// Spalten-Block unten lief durch, aber `unique('name')` scheiterte
|
||||
// erst danach), darf nicht zwei der drei Stellen noch für vorhanden
|
||||
// halten, während die dritte sie schon vermisst.
|
||||
$hadDnsName = Schema::hasColumn('hosts', 'dns_name');
|
||||
|
||||
// Vorabprüfung, bevor irgendetwas angefasst wird — auch vor der
|
||||
// Spalte weiter unten, die selbst schon DDL ist. `hosts.name` trug
|
||||
// noch nie einen eindeutigen Index (siehe create_hosts_table): ein
|
||||
|
|
@ -62,8 +72,20 @@ return new class extends Migration
|
|||
// den diese Prüfung abfangen soll. GROUP BY lässt dieselbe Kollation
|
||||
// entscheiden, die später den Index baut, statt in PHP etwas
|
||||
// nachzubilden, das von der Spalte abdriften kann.
|
||||
$duplicates = DB::table('hosts')
|
||||
->selectRaw("(CASE WHEN dns_name IS NOT NULL AND dns_name <> '' THEN dns_name ELSE name END) as future_name")
|
||||
//
|
||||
// Und auf `$hadDnsName` verzweigt: ist die Spalte schon gefallen (ein
|
||||
// zweiter Anlauf), gibt es den CASE-Ausdruck oben nicht mehr zu
|
||||
// bilden — `name` trägt dann bereits den vollständigen
|
||||
// Übertragungsstand, und die Prüfung läuft direkt auf ihr.
|
||||
$duplicatesQuery = DB::table('hosts');
|
||||
|
||||
if ($hadDnsName) {
|
||||
$duplicatesQuery->selectRaw("(CASE WHEN dns_name IS NOT NULL AND dns_name <> '' THEN dns_name ELSE name END) as future_name");
|
||||
} else {
|
||||
$duplicatesQuery->select('name as future_name');
|
||||
}
|
||||
|
||||
$duplicates = $duplicatesQuery
|
||||
->groupBy('future_name')
|
||||
->havingRaw('COUNT(*) > 1')
|
||||
->pluck('future_name');
|
||||
|
|
@ -94,10 +116,19 @@ return new class extends Migration
|
|||
// Onboarding, nie so weit gekommen) behalten ihren — sie tragen keine
|
||||
// Nummer, die sich übertragen ließe, und das Weiterzählen in
|
||||
// HostName::free() geht an ihnen vorbei.
|
||||
DB::table('hosts')
|
||||
->whereNotNull('dns_name')
|
||||
->where('dns_name', '<>', '')
|
||||
->update(['name' => DB::raw('dns_name')]);
|
||||
//
|
||||
// Übersprungen, wenn `dns_name` laut `$hadDnsName` schon gefallen ist:
|
||||
// dann lief diese Zeile in einem früheren Anlauf schon durch, `name`
|
||||
// trägt den Übertragungsstand bereits, und ein erneuter Versuch
|
||||
// stürbe hier an „Unknown column 'dns_name'" — aus demselben Grund,
|
||||
// aus dem die Vorabprüfung oben verzweigt und der Spalten-Block
|
||||
// weiter unten bedingt läuft.
|
||||
if ($hadDnsName) {
|
||||
DB::table('hosts')
|
||||
->whereNotNull('dns_name')
|
||||
->where('dns_name', '<>', '')
|
||||
->update(['name' => DB::raw('dns_name')]);
|
||||
}
|
||||
|
||||
// Wer nach der Übertragung oben immer noch nicht wie `<rz>-<nn>`
|
||||
// aussieht, hatte keinen `dns_name` zum Übertragen — der getippte
|
||||
|
|
@ -164,13 +195,21 @@ return new class extends Migration
|
|||
// Getrennte Aufrufe: Index löschen, Spalte löschen und Index anlegen in
|
||||
// einem Blueprint bringt SQLite (Testlauf) durcheinander.
|
||||
//
|
||||
// Beide hinter derselben `hasColumn`-Prüfung wie oben und aus
|
||||
// demselben Grund: scheitert der Riegel weiter unten, oder fehlt
|
||||
// einer Rücksicherung genau der `dns_name`-Index, den `dropUnique`
|
||||
// gleich sucht, ist `next_host_number` schon da, `dns_name` aber
|
||||
// noch — ein zweiter Versuch soll wieder bis hierher kommen, nicht
|
||||
// vorher an der Spalte scheitern.
|
||||
if (Schema::hasColumn('hosts', 'dns_name')) {
|
||||
// Hinter demselben `$hadDnsName` wie Vorabprüfung und Übertragung
|
||||
// oben — nicht mehr hinter einer eigenen Schema::hasColumn-Abfrage,
|
||||
// damit alle drei Stellen garantiert dasselbe Urteil fällen. Eine
|
||||
// frühere Fassung dieses Kommentars behauptete, ein Fehlschlag am
|
||||
// Riegel weiter unten ließe `dns_name` hier noch stehen — das war
|
||||
// verkehrt herum: `dropColumn` läuft VOR dem Riegel, nicht danach.
|
||||
// Scheitert `unique('name')`, ist `dns_name` zu diesem Zeitpunkt
|
||||
// längst weg, und ein zweiter Anlauf, der das nicht wüsste, stürbe an
|
||||
// der Vorabprüfung oben statt hier. Mit `$hadDnsName` an einer
|
||||
// einzigen Stelle für den ganzen Lauf entschieden, ist diese
|
||||
// Reihenfolge egal geworden: Vorabprüfung, Übertragung und dieser
|
||||
// Block überspringen ihren `dns_name`-Anteil gemeinsam, sobald die
|
||||
// Spalte einmal gefallen ist — unabhängig davon, WESHALB der Anlauf,
|
||||
// der sie fallen ließ, danach noch gescheitert ist.
|
||||
if ($hadDnsName) {
|
||||
Schema::table('hosts', function (Blueprint $table) {
|
||||
$table->dropUnique(['dns_name']);
|
||||
});
|
||||
|
|
@ -185,6 +224,18 @@ return new class extends Migration
|
|||
// an vorhandenen DATEN scheitern kann statt nur am Schema. Alles, was
|
||||
// danach noch aussteht (die Zeile unten), ist reine Aufräumarbeit an
|
||||
// etwas, das nichts mehr referenziert — sie steht deshalb ZULETZT.
|
||||
//
|
||||
// HIER, nicht mehr am `dns_name`-Block, endet die Wiederholbarkeit
|
||||
// von oben: Vorabprüfung, Übertragung und Spalten-Block sind jetzt
|
||||
// alle an `$hadDnsName` gebunden und damit beliebig oft wiederholbar.
|
||||
// Gelingt aber ausgerechnet dieser Aufruf, und scheitert erst die
|
||||
// reine DELETE-Zeile gleich danach (kein DDL mehr, aber ein
|
||||
// Verbindungsabbruch oder ein Sperr-Timeout bleiben möglich), findet
|
||||
// ein erneuter Anlauf den Index schon vor und stirbt an „Duplicate
|
||||
// key name 'hosts_name_unique'" statt einfach die DELETE-Zeile zu
|
||||
// wiederholen. Unbehandelt gelassen: den Riegel selbst gegen sich
|
||||
// selbst abzusichern ist nicht mehr die billige Ergänzung, die dieser
|
||||
// Fix war, sondern eine eigene Prüfrunde.
|
||||
Schema::table('hosts', function (Blueprint $table) {
|
||||
$table->unique('name');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,15 +47,23 @@ class DatabaseSeeder extends Seeder
|
|||
],
|
||||
);
|
||||
|
||||
// Datacenters — hosts + orders pick their code. next_host_number
|
||||
// steht schon auf 2: die Demoflotte unten belegt die 01 in jedem
|
||||
// Rechenzentrum selbst, und ein Seed-Lauf ist kein Onboarding, das
|
||||
// HostName::claim durchläuft und den Zähler von selbst hochzöge.
|
||||
// Datacenters — hosts + orders pick their code.
|
||||
foreach ([['fsn', 'Falkenstein', 'DE'], ['hel', 'Helsinki', 'FI']] as [$dcCode, $dcName, $dcLocation]) {
|
||||
Datacenter::updateOrCreate(
|
||||
$datacenter = Datacenter::updateOrCreate(
|
||||
['code' => $dcCode],
|
||||
['name' => $dcName, 'location' => $dcLocation, 'active' => true, 'next_host_number' => 2],
|
||||
['name' => $dcName, 'location' => $dcLocation, 'active' => true],
|
||||
);
|
||||
|
||||
// next_host_number gehört NICHT ins zweite Argument von
|
||||
// updateOrCreate(): das schriebe bei jedem erneuten Säen auf 2
|
||||
// zurück, auch auf einer Installation, die längst weiterzählt —
|
||||
// genau die Wiederverwendung, gegen die dieser ganze Umbau
|
||||
// gebaut wurde. max() hält stattdessen den höheren der beiden
|
||||
// Werte: frisch angelegt steht die Spalte auf ihrem
|
||||
// Schema-Default 1, max(1, 2) hebt sie auf 2 (die 01 ist unten
|
||||
// durch die Demoflotte belegt); bestand die Zeile schon und
|
||||
// zählte höher, bleibt sie stehen.
|
||||
$datacenter->update(['next_host_number' => max($datacenter->next_host_number, 2)]);
|
||||
}
|
||||
|
||||
// Demo fleet so the operator console hosts view has content locally.
|
||||
|
|
|
|||
Loading…
Reference in New Issue