enhancement
major
minor
defect
critical
major
minor
critical
#30146
Datenmigration ist beim gleichzeitigen Start mehrerer Cluster-Knoten nicht gegen parallele Ausführung geschützt
== Problem
Werden mehrere Knoten eines Clusters gleichzeitig gestartet (z.B. durch ein Deployment, das alle Knoten auf einmal ausrollt), kann jeder Knoten die automatische Datenmigration (MigrationService) ausführen. Es gibt keine Sperre, die verhindert, dass sich die Migrationsläufe überschneiden. Die Knoten können sich dabei gegenseitig in die Quere kommen bzw. dieselben Migrationen doppelt ausführen.
Der Startup-Token des ClusterManager schützt die Migration nicht, weil die Migration bereits läuft, bevor der Token geholt wird.
== Analyse
=== Startreihenfolge
AbstractStartStopListener.boot():
- ClusterManager.startUpClusterManager() — startet den ClusterManager mit seinen Abhängigkeiten, u.a. ConnectionPoolRegistry.
- startTokenSystem(...) — holt im Cluster-Modus den Startup-Token (aquireStartupContext() über den LockService).
- startupModuleSystem(...) — startet die übrigen Module.
- releaseTokenSystem(...) — gibt den Token frei.
=== Die Migration läuft bereits in Schritt 1
- InitialTableSetup ist @ServiceExtensionPoint(ConnectionPoolRegistry.Module.class).
- MigrationService ist @ServiceExtensionPoint(InitialTableSetup.Module.class).
- ModuleUtil.addDependencies() startet nach einem Modul auch dessen Extensions. Mit ConnectionPoolRegistry (Abhängigkeit des ClusterManager) werden also InitialTableSetup und MigrationService gestartet.
- MigrationService.startUp() ruft direkt migrate(log) auf.
Die Migration läuft damit innerhalb von startUpClusterManager() und vor startTokenSystem().
=== Keine eigene Sperre im MigrationService
- Im MigrationService und im Paket com.top_logic.knowledge.service.migration gibt es kein Lock und keine Cluster-Koordination.
- Die Liste der ausstehenden Migrationen wird bereits im Konstruktor ermittelt (fetchMigrations(), eigene Lese-Connection) und später in startUp() unverändert abgearbeitet.
- Die gespeicherte Schema-Baseline wird dagegen bei jedem Zugriff frisch aus DBProperties gelesen (MigrationContext.getPersistentSchema() → KBSchemaUtil.loadSchema()).
- Migrationsversionen und Baseline sind gewöhnliche Zeilen in DBProperties (DBProperties.setProperty()); Zeilensperren der DB serialisieren höchstens einzelne Schreibzugriffe, nicht den Migrationslauf.
=== Möglicher Ablauf
- Knoten A und B starten gleichzeitig, lesen dieselben gespeicherten Versionen und ermitteln dieselben ausstehenden Migrationen.
- A führt alle Migrationen aus, speichert am Ende über StoreTypeConfiguration die Baseline aus der Anwendungskonfiguration sowie die neuen Versionen und committet.
- B arbeitet seine veraltete Liste ab und wendet die Migrationen auf den bereits migrierten Stand an.
Folgen:
- Nicht idempotente Baseline-Migrationen (XSLT) erzeugen inkonsistente Schemata, z.B. doppelte <key-attributes> (vgl. #30142), sodass B beim Parsen der Baseline scheitert.
- Datenmigrationen können doppelt auf bereits migrierte Daten angewendet werden — dieser Fall fällt nicht zwingend durch einen Fehler auf.
- Bei Datenbanken mit impliziten Commits bei DDL (Oracle, MySQL) können Zwischenstände eines abgebrochenen Laufs persistiert werden.
== Konzeptionelle Einschränkung
Die Migration kann nicht einfach unter den bestehenden Startup-Token gelegt werden: Der Token benötigt LockService und PersistencyLayer, also bereits ein migriertes Schema.
== Lösungsvorschlag
- Eigene, rein JDBC-basierte Sperre für den Migrationslauf, z.B. SELECT ... FOR UPDATE auf eine dedizierte Zeile (z.B. in DBProperties) bzw. ein vergleichbarer DB-Lock, gehalten über den gesamten Lauf inklusive Speichern von Versionen und Baseline.
- Nach Erhalt der Sperre die ausstehenden Migrationen neu ermitteln statt die im Konstruktor berechnete Liste zu verwenden. Ein Knoten, der warten musste, findet dann typischerweise nichts mehr zu tun.
- Timeout bzw. Behandlung eines abgestürzten Knotens, der die Sperre hält (bei einer Transaktionssperre gibt die DB sie beim Verbindungsabbruch frei).