defect
major
#29822
CompactHistoryTask: Wartungsfenster bleibt nach dem Lauf aktiv, Task-Ende geht im Neustart der Persistenzschicht verloren
Problem
Der vom Scheduler gestartete CompactHistoryTask (#27490) betritt mit needs-maintenance-mode das Wartungsfenster. Nach erfolgreichem Lauf wird das Wartungsfenster nicht wieder verlassen; es bleibt auch nach dem Neustart der Persistenzschicht dauerhaft aktiv (beobachtet auf dem Release-Notes-Produktivsystem).
Ursache
- Das Wartungsfenster verlässt nur der Scheduler in taskDone() → cleanupMaintenanceMode(), und nur wenn _maintenanceModeRequester == task. Diese Zuordnung ist reiner Speicherzustand der Scheduler-Instanz.
- Der Task ruft am Ende HistoryCompactionOperation.compactAndRestart() auf; PersistencyMaintenance.scheduleRestart() startet 2 s später das Modul KnowledgeBaseFactory neu und damit auch den Scheduler (Abhängigkeit über PersistencyLayer.Module).
- Das Ende des Task-Threads bemerkt der Dispatcher erst in seiner nächsten Prüfrunde (bis zu polling-interval, Standard 60 s). Beim Herunterfahren wartet Scheduler.shutDown() nur shutdown-join-time (5 s) auf den Dispatcher, dieser schläft vor seinem ersten checkRunning() aber time-to-kill / shutdown-countdown (6 s). taskDone() läuft für den Task daher nicht mehr.
- Der neu gestartete Scheduler kennt keinen Anforderer, das Wartungsfenster ist eine persistente Cluster-Eigenschaft und überdauert den Neustart → niemand verlässt es.
- Mit taskDone() entfallen außerdem das Freigeben der Cluster-Sperre und die Prüfung des Task-Ergebnisses. Die Sperre des alten Knotens bleibt stehen, bis der Aufräum-Task für heruntergefahrene Knoten sie entfernt; bis dahin überspringt der Scheduler den Task.
Verbesserung
Das Wartungsfenster darf erst enden, wenn die Persistenzschicht nach dem Neustart wieder läuft – vorher dürfen sich keine Benutzer anmelden. Kein persistierter Anforderer, stattdessen beendet der ohnehin geplante Neustart das Fenster:
- Der Scheduler erhält eine Möglichkeit, dass ein laufender Task das für ihn betretene Wartungsfenster übernimmt: Die Zuordnung _maintenanceModeRequester wird gelöst, ohne das Fenster zu verlassen; das Ergebnis sagt, ob der Scheduler das Fenster für diesen Task betreten hat.
- PersistencyMaintenance.scheduleRestart erhält die Angabe, ob das Wartungsfenster nach erfolgreichem Neustart verlassen wird. Der Task übergibt das Ergebnis der Übernahme; ein vor einem manuell gestarteten Lauf manuell betretenes Fenster bleibt wie bisher aktiv. Scheitert der Neustart, bleibt das Fenster aktiv.
- Der Neustart beginnt erst, wenn der Scheduler das Ende des Tasks verarbeitet hat (Cluster-Sperre freigegeben, Ergebnis geprüft).
Lösung
- Scheduler.takeOverMaintenanceMode(Task): löst die Zuordnung des Wartungsfensters zum Task, ohne es zu verlassen, und liefert, ob der Scheduler das Fenster für diesen Task betreten hatte. Gegenstück zu waiveMaintenanceMode(Task). Ein manuell betretenes Fenster wird nie übernommen; das gilt auch für das Fenster, das die Administrationsseite „Zeitsteuerung“ beim Starten einer Aufgabe nach Rückfrage selbst betritt (ScheduleTaskCommandHandler).
- Scheduler.runAfterTaskDone(Task, Runnable): Ein laufender Task meldet eine Aktion an, die der Dispatcher-Thread am Ende von taskDone() ausführt – nach dem Freigeben der Cluster-Sperre und der Prüfung des Task-Ergebnisses. Das Task-Ende wird in der nächsten Prüfrunde des Dispatchers verarbeitet, der Neustart beginnt also bis zu ein Polling-Intervall nach dem Ende des Tasks. Eine scheiternde Aktion wird protokolliert; beim Entfernen des Tasks werden seine Aktionen verworfen.
- PersistencyMaintenance.scheduleRestart(Log, boolean leaveMaintenanceWindow): Nach erfolgreichem Neustart wird das Wartungsfenster verlassen, wenn der Aufrufer es verlangt; ein gescheiterter Neustart lässt es aktiv. CompactHistoryDialog und DeletedObjectPurgeOperation übergeben false (dort hat ein Administrator das Fenster manuell betreten).
- CompactHistoryTask: kompaktiert, übernimmt das Wartungsfenster, meldet den Neustart als Aktion nach dem Task-Ende an (mit dem Ergebnis der Übernahme als Angabe, ob das Fenster verlassen wird) und schreibt sein Ergebnis. Ohne Scheduler (nicht angehängt) wird der Neustart direkt angestoßen, ohne das Fenster zu verlassen. HistoryCompactionOperation.compactAndRestart entfällt.
- Scheduler-Herunterfahren: Bereits beendete Tasks werden vor der ersten Wartezeit des Countdowns verarbeitet, sodass taskDone() (und damit die angemeldeten Aktionen) auch läuft, wenn der Scheduler unmittelbar nach dem Task-Ende heruntergefahren wird.
- Entwicklungswerkzeug: .claude/scripts/rebuild-stale.sh folgt auch den Modulen von Profilen, die ein Build durch das Fehlen einer Property aktiviert (full-build der Root-POM, !skipFullBuild); zuvor wurde der Engine-Reaktor nicht enumeriert und veraltete Module blieben unerkannt.
Tests
- TestSchedulerMaintenanceMode (TestingScheduler, allgemeiner CallbackTestTask): Übernahme des vom Scheduler betretenen Fensters (Fenster bleibt nach Task-Ende aktiv), keine Übernahme eines vorher betretenen Fensters, keine Übernahme ohne Wartungsmodus, Verlassen ohne Übernahme wie bisher, Aktion nach Task-Ende läuft erst nach Verarbeitung des Endes (Eintrag wieder wartend) und nur einmal, scheiternde Aktion wird protokolliert und blockiert die nächste nicht.
- Manuell in der Demo-Anwendung (tl-demo, CompactHistoryTask per daily-Schedule vom Scheduler gestartet): Scheduler fordert das Wartungsfenster an, Task kompaktiert und übernimmt das Fenster, Dispatcher verarbeitet das Task-Ende, Neustart der Persistenzschicht, danach „Left the maintenance window“ im Log; Administrationsseite zeigt kein aktives Wartungsfenster und das erfolgreiche Task-Ergebnis.