enhancement
major
minor
defect
critical
major
minor
critical
#30196
Abbrechen eines Hintergrundjobs (TL Views <start-job>, Scheduler-Task, JMS-Consumer) unterbricht den Thread mitten im Datenbankzugriff und schließt eine eingebettete H2-Datenbank (MVStore panic, Anwendung unbenutzbar)
tl-layout-view 8.0.0-alpha12 (Build 817), H2 2.5.252 (eingebettet, Dateidatenbank).
Problem
JobRunner.cancel() setzt das Abbruch-Flag und ruft worker.interrupt() auf, sodass ein Job, der sich im Ruhezustand befindet oder durch E/A-Vorgänge blockiert ist, sofort wieder aktiv wird. Ein Job, der auf der Wissensbasis arbeitet – ein Import, der seine Transaktion festschreibt, wie es ChunkedJobBody pro Chunk tut und wie es der ETS-Import von E-Home (haumacher/ehome#84) am Ende tut –, wird dann mitten in einem Datenbankzugriff unterbrochen. H2s MVStore liest und schreibt seine Datei über einen FileChannel, bei dem es sich um einen InterruptibleChannel handelt: Die Unterbrechung schließt den Kanal (ClosedByInterruptException), der MVStore gerät in eine Panik, und die gemeinsam genutzte Datenbank wird für die gesamte Anwendung geschlossen:
WARN DefaultDBContext – Lang andauernder Commit (Revision 17 mit einer Dauer von 00:06,599)
bei com.top_logic.knowledge.service.AbstractTransaction.commit(AbstractTransaction.java:121)
bei de.haumacher.ehome.etsImport.ImportJobs.importFile(ImportJobs.java:139)
bei de.haumacher.ehome.etsImport.ImportEtsProjectJob.run(ImportEtsProjectJob.java:150)
bei com.top_logic.layout.view.job.JobRunner.run(JobRunner.java:213)
...
org.h2.mvstore.MVStoreException: Das Lesen aus der Datei sun.nio.ch.FileChannelImpl@76958fb3 ist bei 1084339 fehlgeschlagen (Länge -1), gelesen 0, verbleibend 512 [2.5.252/1]
Verursacht durch: java.nio.channels.ClosedChannelException
org.h2.jdbc.JdbcSQLNonTransientException: Allgemeiner Fehler: „org.h2.mvstore.MVStoreException: Das Schreiben in sun.nio.ch.FileChannelImpl@76958fb3 ist fehlgeschlagen; Länge 24576 bei 5595136 [2.5.252/2]“
FEHLER ClusterManager – Das erneute Abrufen des Cluster-Managers ist fehlgeschlagen.
Reproduktion: Ein abbrechbarer <start-job>, dessen Hauptteil eine große Transaktion festschreibt; drücken Sie „Abbrechen“, während der Commit läuft. Danach schlägt jeder Datenbankzugriff der Anwendung fehl, bis sie neu gestartet wird. Der gleiche Mechanismus wurde für den Scheduler-Thread in #17783 behoben.
Ein zweiter Weg zum gleichen Fehler: Die TL-Script-Funktion `sleep()` setzt das Interrupt-Flag nach dem Aufwachen zurück. Ein Skript, das nach dem Schlafzustand und vor seinem nächsten Fortschrittsbericht auf die Datenbank zugreift, beginnt diesen Zugriff mit gesetztem Flag, wodurch der Kanal sofort geschlossen wird.
Die gleiche Gefahr besteht überall dort, wo die Engine einen Thread unterbricht, der möglicherweise auf die Datenbank zugreift: wenn der Scheduler eine zu lange laufende Aufgabe stoppt (ScheduledThread.signalStop()), beim Herunterfahren der JMS-Konsumenten (JMSService) oder durch den Timer des Wartungsfensters.
Erwartet
Das Abbrechen eines Jobs unterbricht diesen niemals, während er auf die Datenbank zugreift. Beispielsweise unterbricht der Runner nur, während der Hauptteil wartet (Sleep, eine JobMonitor-Methode, die blockiert), oder der Hauptteil gibt an, dass er durch eine Unterbrechung geweckt werden möchte (Bus-E/A, Sleep), und die Standardeinstellung ist die kooperative Abbruchbehandlung ausschließlich an den Berichtspunkten. In der Dokumentation zu <start-job> (commands.md, „Abbruch erfolgt kooperativ“) sollte das Risiko benannt werden, solange es besteht.
Workaround in E-Home
Die Import-Körper führen ihre Arbeit in einem eigenen Thread aus und lassen den Worker nur darauf warten (ImportJobs#shielded): Die Unterbrechung erreicht den wartenden Worker, die Abbruchanforderung wird vom Import-Thread an seinem nächsten Berichtspunkt über denselben JobMonitor beantwortet.
Lösung
CancelScope (com.top_logic.basic.thread): Das Abbrechen einer Arbeit markiert diese lediglich als abgebrochen. Es unterbricht den Thread ausschließlich, solange er sich in einem Abschnitt befindet, der ausdrücklich als unterbrechbar deklariert wurde (interruptibly(...), await(...), statisch über den an den Thread gebundenen Scope: CancelScope.runInterruptibly(...), CancelScope.sleep(...)). Wer nach dem Abbruch in einen solchen Abschnitt eintritt, scheitert sofort (AbortExecutionException); beim Verlassen hebt der Abschnitt eine von ihm ausgelöste Unterbrechung unter derselben Sperre auf, sodass das Interrupt-Flag niemals in einen nachfolgenden Datenbankzugriff gelangt. Der Mechanismus wurde aus `CancellationToken.whileBlocked ` (tl-ai-tool-api) übernommen; `CancellationToken` erweitert `CancelScope` (wirft weiterhin `CancellationException`), `RunCancellationBinding` bindet das Token gleichzeitig als aktuellen `CancelScope`. Die Thread-Binding-Hilfsklassen ThreadLocalBinding, BindingRegistry und BindingSnapshot befinden sich dafür nun in com.top_logic.basic.thread.
Jobs der TL-Ansichten: JobRunner führt den Body in einem CancelScope aus; cancel() unterbricht den Worker niemals. Der Body endet am nächsten Meldepunkt des JobMonitor; ein Job, der vor dem Start seines Bodys abgebrochen wird, endet abgebrochen, ohne den Body auszuführen. Ein Java-Body umschließt blockierende Wartevorgänge (Bus-I/O, HTTP, Futures) mit `CancelScope.runInterruptibly(...)`, niemals Datenbankoperationen.
TL-Script sleep(): wartet als unterbrechbarer Abschnitt des aktuellen Scopes und beendet einen abgebrochenen Job sofort (der Job endet als abgebrochen), anstatt das Interrupt-Flag wieder zu setzen. Außerhalb eines Scopes unverändert.
Scheduler: Jeder Lauf einer Aufgabe (BatchImpl) verfügt über einen eigenen CancelScope; dieser ist das einzige Stop-Signal der Aufgabe: signalStop() bricht ihn ab, getShouldStop() liest ihn aus. ScheduledThread bindet ihn während der Ausführung an den Thread, zusammengesetzte Tasks binden den Scope des jeweiligen Kind-Tasks. Dadurch wecken alle Stopp-Wege – Überschreiten der Laufzeit, der Stopp-Button der Administration (Scheduler.stopTask), Herunterfahren, Stopp eines Kind-Tasks – ein sleep() bzw. einen unterbrechbaren Abschnitt des Tasks sofort; der Thread wird nie unterbrochen. Ein auf einem anderen Cluster-Knoten angeforderter Stopp wird lokal bei der nächsten Abfrage von getShouldStop() erkannt und bricht dann den Scope ab; ein bereits laufendes Warten weckt er nicht.
JMS: Beim Herunterfahren beendet das Schließen des Consumers dessen blockierendes Empfangen; der Consumer-Thread wird nicht mehr unterbrochen.
Wartungsfenster: Nur das Warten des Timers ist unterbrechbar; ein Timer, der nach seinem Warten bereits ausgelöst wurde, aktiviert das Wartungsfenster nicht mehr.
Geprüft, unverändert: Log-Threads des MemoryObserver / MemoryObserverThread und der Katalog-Lader des LLMProviderService greifen weder auf die Datenbank noch auf einen gemeinsam genutzten Dateikanal zu.
Dokumentation: commands.md („Cancellation is cooperative“) sowie JavaDoc zu JobControl, JobBody, JobMonitor.
Migration
- Die Klassen ThreadLocalBinding, BindingRegistry und BindingSnapshot wurden von com.top_logic.tl.ai.core.binding nach com.top_logic.basic.thread verschoben. Passen Sie die Importe in Ihrem eigenen Code entsprechend an.
- Code, der davon ausgeht, dass das Abbrechen eines <start-job>-Jobs oder das Stoppen einer Scheduler-Aufgabe den Thread unterbricht (eigenes Thread.sleep, Object.wait, blockierende E/A, Future.get), wird nicht mehr fortgesetzt. Führen Sie solche Wartevorgänge mit ` CancelScope.sleep(...) ` bzw. `CancelScope.runInterruptibly(...) ` aus – beziehen Sie dabei niemals Datenbankzugriffe mit ein.