enhancement
major
minor
defect
critical
major
minor
critical
#30196
Canceling a background job (TL Views <start-job>, Scheduler Task, JMS Consumer) interrupts the thread in the middle of a database operation and closes an embedded H2 database (MVStore panic, application becomes unusable)
tl-layout-view 8.0.0-alpha12 (build 817), H2 2.5.252 (embedded, file database).
Problem
JobRunner.cancel() sets the cancel flag and calls worker.interrupt(), so that a job sleeping or blocked in I/O wakes at once. A job that works on the knowledge base - an import committing its transaction, as ChunkedJobBody does per chunk and as E-Home's ETS import (haumacher/ehome#84) does at its end - is then interrupted in the middle of a database access. H2's MVStore reads and writes its file through a FileChannel, which is an InterruptibleChannel: the interrupt closes the channel (ClosedByInterruptException), the MVStore panics, and the shared database is closed for the whole application:
WARN DefaultDBContext - Long running commit (revision 17 lasting 00:06,599) at com.top_logic.knowledge.service.AbstractTransaction.commit(AbstractTransaction.java:121) at de.haumacher.ehome.etsImport.ImportJobs.importFile(ImportJobs.java:139) at de.haumacher.ehome.etsImport.ImportEtsProjectJob.run(ImportEtsProjectJob.java:150) at com.top_logic.layout.view.job.JobRunner.run(JobRunner.java:213) ... org.h2.mvstore.MVStoreException: Reading from file sun.nio.ch.FileChannelImpl@76958fb3 failed at 1084339 (length -1), read 0, remaining 512 [2.5.252/1] Caused by: java.nio.channels.ClosedChannelException org.h2.jdbc.JdbcSQLNonTransientException: General error: "org.h2.mvstore.MVStoreException: Writing to sun.nio.ch.FileChannelImpl@76958fb3 failed; length 24576 at 5595136 [2.5.252/2]" ERROR ClusterManager - Failed to refetch cluster manager.
Reproduction: a cancelable <start-job> whose body commits a large transaction; press Cancel while the commit runs. After that every database access of the application fails until it is restarted. The same mechanism was fixed for the scheduler thread in #17783.
A second path to the same failure: TL-Script sleep() re-sets the interrupt flag after being woken. A script that accesses the database after the sleep, before its next progress report, starts that access with the flag set, which closes the channel immediately.
The same hazard exists wherever the engine interrupts a thread that may be accessing the database: the scheduler stopping a task that runs too long (ScheduledThread.signalStop()), the shutdown of the JMS consumers (JMSService), the timer of the maintenance window.
Expected
Cancelling a job never interrupts it while it accesses the database. E.g. the runner interrupts only while the body waits (sleep, a JobMonitor method that blocks), or the body declares that it wants to be woken by an interrupt (bus I/O, sleeping), and the default is the cooperative cancel at the report points alone. The documentation of <start-job> (commands.md, "Cancellation is cooperative") should name the hazard as long as it exists.
Workaround in E-Home
The import bodies run their work on a thread of their own and let the worker only wait for it (ImportJobs#shielded): the interrupt reaches the waiting worker, the cancel is answered by the import thread at its next report point through the same JobMonitor.
Lösung
CancelScope (com.top_logic.basic.thread): Abbrechen einer Arbeit markiert sie nur als abgebrochen. Den Thread unterbricht es ausschließlich, solange er sich in einem ausdrücklich als unterbrechbar erklärten Abschnitt befindet (interruptibly(...), await(...), statisch über den an den Thread gebundenen Scope: CancelScope.runInterruptibly(...), CancelScope.sleep(...)). Wer einen solchen Abschnitt nach dem Abbruch betritt, scheitert sofort (AbortExecutionException); beim Verlassen nimmt der Abschnitt eine von ihm ausgelöste Unterbrechung unter derselben Sperre zurück, so dass das Interrupt-Flag nie in einen folgenden Datenbankzugriff gelangt. Der Mechanismus ist aus CancellationToken.whileBlocked (tl-ai-tool-api) heruntergezogen; CancellationToken erweitert CancelScope (wirft weiterhin CancellationException), RunCancellationBinding bindet das Token zugleich als aktuellen CancelScope. Die Thread-Binding-Hilfsklassen ThreadLocalBinding, BindingRegistry und BindingSnapshot liegen dafür jetzt in com.top_logic.basic.thread.
Jobs der TL Views: JobRunner führt den Body in einem CancelScope aus; cancel() unterbricht den Worker nie. 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(...), nie Datenbankarbeit.
TL-Script sleep(): wartet als unterbrechbarer Abschnitt des aktuellen Scopes und beendet einen abgebrochenen Job sofort (der Job endet als abgebrochen), statt das Interrupt-Flag wieder zu setzen. Außerhalb eines Scopes unverändert.
Scheduler: Jeder Lauf eines Tasks (BatchImpl) hat einen eigenen CancelScope; er ist das einzige Stop-Signal des Tasks: signalStop() bricht ihn ab, getShouldStop() liest ihn. ScheduledThread bindet ihn während des Laufs an den Thread, zusammengesetzte Tasks binden den Scope des jeweiligen Kind-Tasks. Damit wecken alle Stop-Wege - Überschreiten der Laufzeit, der Stop-Knopf der Administration (Scheduler.stopTask), Herunterfahren, Stop eines Kind-Tasks - ein sleep() bzw. einen unterbrechbaren Abschnitt des Tasks sofort; der Thread wird nie unterbrochen. Ein auf einem anderen Cluster-Knoten angeforderter Stop wird lokal beim nächsten Abfragen 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 abgelöst wurde, schaltet das Wartungsfenster nicht mehr ein.
Geprüft, unverändert: Log-Threads des MemoryObserver / MemoryObserverThread und der Katalog-Lader des LLMProviderService greifen weder auf die Datenbank noch auf einen geteilten Dateikanal zu.
Dokumentation: commands.md ("Cancellation is cooperative") sowie JavaDoc von JobControl, JobBody, JobMonitor.
Migration
- Die Klassen ThreadLocalBinding, BindingRegistry und BindingSnapshot sind von com.top_logic.tl.ai.core.binding nach com.top_logic.basic.thread umgezogen. Importe in eigenem Code anpassen.
- Code, der darauf vertraut, dass das Abbrechen eines <start-job>-Jobs oder das Stoppen eines Scheduler-Tasks den Thread unterbricht (eigenes Thread.sleep, Object.wait, blockierendes I/O, Future.get), wird nicht mehr geweckt. Solche Wartevorgänge mit CancelScope.sleep(...) bzw. CancelScope.runInterruptibly(...) ausführen - nie Datenbankzugriffe darin einschließen.