Blog

Exit-Code 0, falsche Datenbank

· Datenbanken · Praxis

Der unangenehmste Fehler ist nicht der, bei dem etwas abstürzt. Es ist der, bei dem alles grün meldet und trotzdem das Falsche passiert ist.

Genau so einer steckte in RayMigrator, und er ist lehrreich genug, um ihn aufzuschreiben — auch wenn er uns selbst betroffen hat.

Das Symptom

Ein migrate-up läuft durch. Exit-Code 0. Das Protokoll zeigt angewandte Migrationen, keine Warnung, keine Auffälligkeit.

Nur: Die Migrationen wurden auf der Datenbank eines anderen Produkts ausgeführt.

Nichts an diesem Lauf war technisch fehlerhaft. Die Verbindung stand, die Skripte waren gültig, die Transaktionen sauber. Das Werkzeug hat exakt das getan, was seine Konfiguration ihm gesagt hat. Die Konfiguration war nur nicht die, die in den Dateien stand.

Die Ursache: Arrays verschmelzen nach Position

RayMigrator liest seine Konfiguration aus einer Hierarchie von appsettings*.json-Dateien — eine Basisdatei, darüber umgebungs- und produktspezifische. Der Standardmechanismus von .NET führt die zusammen.

Bei Objekten tut er das nach Schlüsselnamen. Bei Arrays nach Position.

Das ist der Punkt. Eine Konfiguration, die Produkte als Array beschreibt, sieht so aus:

{
  "Products": [
    { "Alias": "shop",      "TargetGroups": [ … ] },
    { "Alias": "warehouse", "TargetGroups": [ … ] }
  ]
}

Wenn eine Umgebungsdatei nun nur das eine Produkt nennt, das sie ändern will:

{
  "Products": [
    { "Alias": "warehouse", "TargetGroups": [ … ] }
  ]
}

… dann landet dieser Eintrag auf Position 0 — und überschreibt damit shop. Nicht warehouse. Die Zielgruppen und Verbindungszeichenfolgen des Lagerprodukts hängen danach am Shop.

Derselbe Effekt tritt ein, wenn eine Datei die Produkte lediglich in anderer Reihenfolge aufführt. Niemand denkt beim Schreiben einer Konfigurationsdatei über die Reihenfolge von Array-Elementen nach. Warum auch — bei Objekten spielt sie keine Rolle.

Warum es niemandem auffiel

Alle Bedingungen zusammen ergeben einen Fehler, der sich gut versteckt:

  • Er tritt nur auf, wenn eine überlagernde Datei die Elemente unvollständig oder in anderer Reihenfolge nennt — also genau dann, wenn jemand die Konfiguration sauber kürzen wollte
  • Er erzeugt keine Fehlermeldung, weil das Ergebnis ein gültiges Konfigurationsobjekt ist
  • Er fällt bei einem Produkt nicht auf, sondern erst ab zweien
  • In der Entwicklung mit einer einzigen Umgebung tritt er nie auf

Das ist das Muster hinter vielen teuren Fehlern: Sie brauchen eine Kombination, die in der Entwicklung nicht vorkommt.

Die Lösung: nach Alias statt nach Position

Seit Version 0.15.0 führt RayMigrator Arrays, deren Elemente ein Alias-Feld tragen, nach diesem Alias zusammen — groß- und kleinschreibungsunabhängig, auf jeder Verschachtelungsebene. Eine spätere Datei überschreibt das Element mit demselben Alias Eigenschaft für Eigenschaft, neue Aliase kommen hinzu, nicht genannte bleiben erhalten.

Arrays ohne Alias — Protokollsenken etwa — werden weiterhin als Ganzes ersetzt. Das ist verständlicher als eine Teilzusammenführung, hat aber eine Folge: Wer bisher eine einzelne Senke geändert und die übrigen geerbt hat, muss das Array jetzt vollständig wiederholen. Diese Bruchstelle war uns lieber als eine Regel, die von der Elementform abhängt.

Was wir daraus mitnehmen

Positionsabhängigkeit in Konfiguration ist eine Falle. Wo Elemente eine fachliche Identität haben, muss die Zusammenführung dieser Identität folgen — nicht dem Index.

Exit-Code 0 ist keine Zusicherung. Ein Werkzeug, das Produktionsdatenbanken anfasst, sollte beim Start protokollieren, worauf es gleich arbeitet — aufgelöste Verbindung, aufgelöstes Produkt, aufgelöste Zielgruppe. Dieselbe Prüfung, die ein Mensch vor dem Loslassen macht.

Fehler, die nur in Kombination auftreten, brauchen Tests in Kombination. Eine einzelne Konfigurationsdatei zu testen, hätte diesen Fehler nie gezeigt. Die Testfälle für die Zusammenführung liegen deshalb jetzt als Golden Files im Repository und laufen in zwei Testebenen.


Details zur Änderung stehen im Changelog unter 0.15.0.