H2 schließt seine Datenbank nicht mehr unter dem laufenden Prozess #13

Open
claude-code wants to merge 1 commits from h2-close-delay into main
Collaborator

Die Datasource-URL enthielt kein DB_CLOSE_DELAY. H2 nimmt dann 0 und schließt die Datei-Datenbank, sobald die letzte Verbindung geht — bei unbenutzter Anwendung zieht der Hikari-Pool genau das zurück. Der laufende Prozess schloss und öffnete seine Datenbank also wiederholt unter sich selbst.

Die Ursache des Vorfalls vom 2026-10-21 ist damit NICHT belegt

Klärung kam vor Behebung, und das Ergebnis ist negativ: ein leerer Pool bringt H2 nachweislich dazu, die Datenbank zu schließen — der nächste Schreibvorgang öffnet sie aber einfach wieder und gelingt. Das gilt auch bei 8000 Öffnen-und-Schließen-Durchläufen auf vier Threads, die das Rennen zwischen schließender und öffnender Sitzung absichtlich suchen. Die führende Erklärung trägt in dieser Form nicht.

Zwei Annahmen sind dabei als falsch belegt: H2 2.4.240 vergibt für 90xxx-Fehler den Fehlercode selbst als SQLState, nicht eine 08er-Klasse, und von den vermuteten Kandidaten erzeugen OBJECT_CLOSED (90007) und IO_EXCEPTION (90028/90031) gar keine JdbcSQLNonTransientConnectionException, sondern JdbcSQLNonTransientException.

Weiter eingegrenzt hat die Ursachenkette: UiErrorConfiguration protokolliert die unterste Ursache, und protokolliert wurde die H2-Klasse selbst — die Exception hatte also keine weitere Ursache.

Auslöser Exception-Klasse unterste Ursache
DATABASE_IS_CLOSED (90098) JdbcSQLNonTransientConnectionException keine
OutOfMemoryError → OUT_OF_MEMORY (90108) JdbcSQLNonTransientConnectionException java.lang.OutOfMemoryError
IOException → IO_EXCEPTION_2 (90031) JdbcSQLNonTransientException java.io.IOException
MVStoreException JdbcSQLNonTransientException org.h2.mvstore.MVStoreException

Speichermangel — naheliegend bei -Xmx384m — hätte java.lang.OutOfMemoryError hinterlassen. Übrig bleibt ein ursachenloses DATABASE_IS_CLOSED. Das Symptombild zeigt auf eine geschlossene Datenbank; der Weg dorthin bleibt offen, und dass Lesen durchgehend funktionierte, ist mit einer geschlossenen Datenbank ebenfalls nicht erklärt.

Belegt ist die beseitigte Vorbedingung, nicht der Fix. Die Härtung entfernt eine unnötige Unruhe im Betrieb, die den Vorfall nicht erklärt.

Begründung der Wahl zwischen den beiden Wegen

Gewählt: ;DB_CLOSE_DELAY=-1 an der Standard-URL in application.properties.

Verworfen: spring.datasource.hikari.connection-init-sql=SET DB_CLOSE_DELAY -1. Der Weg wirkt zwar unabhängig von der URL, aber SET DB_CLOSE_DELAY ist in H2 eine dauerhafte Einstellung der Datenbankdatei — nachgemessen: nach einem Lauf mit dieser Anweisung meldet auch eine Verbindung ohne sie DB_CLOSE_DELAY = -1. Die Einstellung wandert damit unsichtbar in die Daten und gilt anschließend auch für Werkzeuge, die die Datei kurz öffnen, etwa den Offline-Befehl AdminRecovery. Eine Konfigurationszeile, die sich zurücknehmen lässt, ist einer in die Datei geschriebenen Einstellung vorzuziehen, die das nicht tut; hinzu kommt, dass die Anweisung bei jeder neuen Verbindung läuft.

Der einzige Nachteil des gewählten Wegs — eine Umgebungsüberschreibung könnte den Parameter ersetzen — ist geprüft und geschlossen: der Lesezugriff auf ~/.config/werkjournal/environment zeigt, dass in Produktion keine Überschreibung der Datasource-URL gesetzt ist (die Datei führt nur Gastmodus, Bootstrap-Code und die Push-Schlüssel). Die Standard-URL ist dort also maßgeblich, und die Preflight nimmt eine künftige Überschreibung nur noch mit demselben Parameter an.

Preflight: Sicherungsgarantie bleibt

Bisher verglich die Preflight eine Überschreibung als Zeichenkette mit jdbc:h2:file:./data/werkjournal und hätte die neue URL abgelehnt. check_database_url prüft jetzt den Dateipfad: relativ wie absolut normalisiert, und er muss genau auf die verwaltete Datei fallen, weil das Deployment den Dienst stoppt und das geschlossene Datenverzeichnis in ein Rollback-Bündel kopiert. Weiterhin abgelehnt: Umwege über .., andere Dateinamen im selben Verzeichnis, mem: und tcp:. Zusätzlich muss DB_CLOSE_DELAY=-1 mitgeführt werden, und jeder weitere Parameter wird abgelehnt.

Letzteres schützt vor allem DB_CLOSE_ON_EXIT: die Vorgabe TRUE bleibt bewusst stehen, weil der Shutdown-Hook die Datenbank beim Prozessende sauber schließt und genau darauf die konsistente Sicherung beruht. Nachgemessen: auch mit DB_CLOSE_DELAY=-1 wird beim Prozessende geschlossen; -1 verhindert nur das Schließen im Betrieb. DB_CLOSE_ON_EXIT=FALSE würde das Datenverzeichnis ohne diese Zusicherung zurücklassen und wird deshalb zurückgewiesen.

Keine Migration, kein Schemaeingriff. Die fest eingetragene URL in AdminRecovery bleibt unverändert: dieser Offline-Befehl läuft kurzlebig ohne Pool.

Nachweise

  • H2ConnectionLifecycleTest: liest die Parameter der konfigurierten Standard-URL aus application.properties, baut einen Pool in der Form der Produktion (maximumPoolSize=4) und zieht dessen Verbindungen zurück. Ohne Härtung zählt ein DATABASE_EVENT_LISTENER mindestens eine Schließung der Datenbank, mit der konfigurierten URL keine. „Mindestens“, weil Hikari beim Aufbau des Pools eine Probeverbindung öffnet und wieder schließt, die ihrerseits schon eine Schließung auslösen kann.
  • tests/operations/test_update.py, neuer ManagedDatabaseUrlTest: die verwaltete Datei wird relativ, absolut und über einen Umweg akzeptiert, solange sie offen bleibt; Pfade außerhalb des Sicherungsverzeichnisses, andere Dateinamen, mem: und tcp: werden abgelehnt; ohne DB_CLOSE_DELAY=-1 wird abgelehnt; DB_CLOSE_ON_EXIT=FALSE, CIPHER und ein Parameter ohne Wert werden abgelehnt.
  • test_runtime_preflight_rejects_external_database_and_inline_overrides: derselbe Weg durch die vollständige Preflight über die geschützte Umgebungsdatei, einschließlich der jetzt angenommenen gehärteten Überschreibung.
  • JAVA_HOME=~/opt/jdk25 ./tools/ci-jvm --without-firefox grün: BUILD SUCCESSFUL in 8m 15s, 72 Betriebstests.

Details: docs/implementation/h2-connection-lifecycle.md. Werkbaum: #h2-close-delay erledigt, #h2-failure-proof neu offen.

Hinweis zur Version

0.1.126-SNAPSHOT, abgestimmt mit dem parallelen Branch home-heading-one-line, der 0.1.125 nimmt. Wer zuletzt merged, sollte die Version auf main unmittelbar vorher prüfen und die nächste freie Nummer nehmen.

🤖 Generated with Claude Code

Die Datasource-URL enthielt kein `DB_CLOSE_DELAY`. H2 nimmt dann 0 und schließt die Datei-Datenbank, sobald die letzte Verbindung geht — bei unbenutzter Anwendung zieht der Hikari-Pool genau das zurück. Der laufende Prozess schloss und öffnete seine Datenbank also wiederholt unter sich selbst. ## Die Ursache des Vorfalls vom 2026-10-21 ist damit NICHT belegt Klärung kam vor Behebung, und das Ergebnis ist negativ: ein leerer Pool bringt H2 nachweislich dazu, die Datenbank zu schließen — der nächste Schreibvorgang öffnet sie aber einfach wieder und gelingt. Das gilt auch bei 8000 Öffnen-und-Schließen-Durchläufen auf vier Threads, die das Rennen zwischen schließender und öffnender Sitzung absichtlich suchen. Die führende Erklärung trägt in dieser Form nicht. Zwei Annahmen sind dabei als falsch belegt: H2 2.4.240 vergibt für 90xxx-Fehler den Fehlercode selbst als SQLState, nicht eine 08er-Klasse, und von den vermuteten Kandidaten erzeugen `OBJECT_CLOSED` (90007) und `IO_EXCEPTION` (90028/90031) gar keine `JdbcSQLNonTransientConnectionException`, sondern `JdbcSQLNonTransientException`. Weiter eingegrenzt hat die Ursachenkette: `UiErrorConfiguration` protokolliert die unterste Ursache, und protokolliert wurde die H2-Klasse selbst — die Exception hatte also keine weitere Ursache. | Auslöser | Exception-Klasse | unterste Ursache | | --- | --- | --- | | `DATABASE_IS_CLOSED` (90098) | `JdbcSQLNonTransientConnectionException` | keine | | `OutOfMemoryError` → `OUT_OF_MEMORY` (90108) | `JdbcSQLNonTransientConnectionException` | `java.lang.OutOfMemoryError` | | `IOException` → `IO_EXCEPTION_2` (90031) | `JdbcSQLNonTransientException` | `java.io.IOException` | | `MVStoreException` | `JdbcSQLNonTransientException` | `org.h2.mvstore.MVStoreException` | Speichermangel — naheliegend bei `-Xmx384m` — hätte `java.lang.OutOfMemoryError` hinterlassen. Übrig bleibt ein ursachenloses `DATABASE_IS_CLOSED`. Das Symptombild zeigt auf eine geschlossene Datenbank; der Weg dorthin bleibt offen, und dass Lesen durchgehend funktionierte, ist mit einer geschlossenen Datenbank ebenfalls nicht erklärt. **Belegt ist die beseitigte Vorbedingung, nicht der Fix.** Die Härtung entfernt eine unnötige Unruhe im Betrieb, die den Vorfall nicht erklärt. ## Begründung der Wahl zwischen den beiden Wegen **Gewählt: `;DB_CLOSE_DELAY=-1` an der Standard-URL in `application.properties`.** **Verworfen: `spring.datasource.hikari.connection-init-sql=SET DB_CLOSE_DELAY -1`.** Der Weg wirkt zwar unabhängig von der URL, aber `SET DB_CLOSE_DELAY` ist in H2 eine **dauerhafte Einstellung der Datenbankdatei** — nachgemessen: nach einem Lauf mit dieser Anweisung meldet auch eine Verbindung ohne sie `DB_CLOSE_DELAY = -1`. Die Einstellung wandert damit unsichtbar in die Daten und gilt anschließend auch für Werkzeuge, die die Datei kurz öffnen, etwa den Offline-Befehl `AdminRecovery`. Eine Konfigurationszeile, die sich zurücknehmen lässt, ist einer in die Datei geschriebenen Einstellung vorzuziehen, die das nicht tut; hinzu kommt, dass die Anweisung bei jeder neuen Verbindung läuft. Der einzige Nachteil des gewählten Wegs — eine Umgebungsüberschreibung könnte den Parameter ersetzen — ist geprüft und geschlossen: der Lesezugriff auf `~/.config/werkjournal/environment` zeigt, dass in Produktion **keine** Überschreibung der Datasource-URL gesetzt ist (die Datei führt nur Gastmodus, Bootstrap-Code und die Push-Schlüssel). Die Standard-URL ist dort also maßgeblich, und die Preflight nimmt eine künftige Überschreibung nur noch mit demselben Parameter an. ## Preflight: Sicherungsgarantie bleibt Bisher verglich die Preflight eine Überschreibung als Zeichenkette mit `jdbc:h2:file:./data/werkjournal` und hätte die neue URL abgelehnt. `check_database_url` prüft jetzt den **Dateipfad**: relativ wie absolut normalisiert, und er muss genau auf die verwaltete Datei fallen, weil das Deployment den Dienst stoppt und das geschlossene Datenverzeichnis in ein Rollback-Bündel kopiert. Weiterhin abgelehnt: Umwege über `..`, andere Dateinamen im selben Verzeichnis, `mem:` und `tcp:`. Zusätzlich muss `DB_CLOSE_DELAY=-1` mitgeführt werden, und jeder weitere Parameter wird abgelehnt. Letzteres schützt vor allem `DB_CLOSE_ON_EXIT`: die Vorgabe `TRUE` bleibt bewusst stehen, weil der Shutdown-Hook die Datenbank beim Prozessende sauber schließt und genau darauf die konsistente Sicherung beruht. Nachgemessen: auch mit `DB_CLOSE_DELAY=-1` wird beim Prozessende geschlossen; `-1` verhindert nur das Schließen im Betrieb. `DB_CLOSE_ON_EXIT=FALSE` würde das Datenverzeichnis ohne diese Zusicherung zurücklassen und wird deshalb zurückgewiesen. Keine Migration, kein Schemaeingriff. Die fest eingetragene URL in `AdminRecovery` bleibt unverändert: dieser Offline-Befehl läuft kurzlebig ohne Pool. ## Nachweise - `H2ConnectionLifecycleTest`: liest die Parameter der konfigurierten Standard-URL aus `application.properties`, baut einen Pool in der Form der Produktion (`maximumPoolSize=4`) und zieht dessen Verbindungen zurück. Ohne Härtung zählt ein `DATABASE_EVENT_LISTENER` mindestens eine Schließung der Datenbank, mit der konfigurierten URL keine. „Mindestens“, weil Hikari beim Aufbau des Pools eine Probeverbindung öffnet und wieder schließt, die ihrerseits schon eine Schließung auslösen kann. - `tests/operations/test_update.py`, neuer `ManagedDatabaseUrlTest`: die verwaltete Datei wird relativ, absolut und über einen Umweg akzeptiert, solange sie offen bleibt; Pfade außerhalb des Sicherungsverzeichnisses, andere Dateinamen, `mem:` und `tcp:` werden abgelehnt; ohne `DB_CLOSE_DELAY=-1` wird abgelehnt; `DB_CLOSE_ON_EXIT=FALSE`, `CIPHER` und ein Parameter ohne Wert werden abgelehnt. - `test_runtime_preflight_rejects_external_database_and_inline_overrides`: derselbe Weg durch die vollständige Preflight über die geschützte Umgebungsdatei, einschließlich der jetzt angenommenen gehärteten Überschreibung. - `JAVA_HOME=~/opt/jdk25 ./tools/ci-jvm --without-firefox` grün: `BUILD SUCCESSFUL in 8m 15s`, 72 Betriebstests. Details: `docs/implementation/h2-connection-lifecycle.md`. Werkbaum: `#h2-close-delay` erledigt, `#h2-failure-proof` neu offen. ## Hinweis zur Version 0.1.126-SNAPSHOT, abgestimmt mit dem parallelen Branch `home-heading-one-line`, der 0.1.125 nimmt. Wer zuletzt merged, sollte die Version auf `main` unmittelbar vorher prüfen und die nächste freie Nummer nehmen. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mi added 1 commit 2026-10-23 13:38:46 +02:00
Die Datasource-URL enthielt kein DB_CLOSE_DELAY. H2 nimmt dann 0 und schließt die Datei-Datenbank,
sobald die letzte Verbindung geht — bei unbenutzter Anwendung zieht der Hikari-Pool genau das zurück.
Der laufende Prozess schloss und öffnete seine Datenbank also wiederholt unter sich selbst.

Das war der Anlass, und die Klärung kam vor der Behebung. Ergebnis: ein leerer Pool bringt H2
nachweislich dazu, die Datenbank zu schließen — der nächste Schreibvorgang öffnet sie aber einfach
wieder und gelingt. Das gilt auch bei 8000 Öffnen-und-Schließen-Durchläufen auf vier Threads, die das
Rennen absichtlich suchen. Die führende Erklärung trägt in dieser Form nicht.

Zwei Annahmen des Auftrags sind dabei als falsch belegt: H2 2.4.240 vergibt für 90xxx-Fehler den
Fehlercode selbst als SQLState, nicht eine 08er-Klasse, und von den genannten Kandidaten erzeugen
OBJECT_CLOSED (90007) und IO_EXCEPTION (90028/90031) gar keine JdbcSQLNonTransientConnectionException.

Weiter eingegrenzt hat die Ursachenkette: UiErrorConfiguration protokolliert die unterste Ursache, und
protokolliert wurde die H2-Klasse selbst. Ein Speichermangel hätte dort java.lang.OutOfMemoryError
hinterlassen, ein Schreibfehler der Ablage einen MVStoreException oder IOException. Übrig bleibt ein
ursachenloses DATABASE_IS_CLOSED. Das Symptombild zeigt auf eine geschlossene Datenbank, der Weg
dorthin bleibt offen — und dass Lesen durchgehend funktionierte, ist damit ebenfalls nicht erklärt.

- **`;DB_CLOSE_DELAY=-1` an der Standard-URL, nicht `connection-init-sql`.** `SET DB_CLOSE_DELAY` ist
  in H2 eine dauerhafte Einstellung der Datenbankdatei: nach einem Lauf meldet auch eine Verbindung
  ohne die Anweisung -1. Die Einstellung wanderte damit unsichtbar in die Daten und gälte auch für
  Werkzeuge, die die Datei kurz öffnen. Eine Konfigurationszeile, die sich zurücknehmen lässt, ist
  dem vorzuziehen. Der Lesezugriff auf die geschützte Umgebungsdatei in Produktion zeigt, dass dort
  keine Überschreibung der Datasource-URL gesetzt ist; die Standard-URL ist also maßgeblich.
- **Die Preflight prüft den Dateipfad statt der Zeichenkette.** Ihre Zusicherung bleibt: der Pfad aus
  der URL wird — relativ wie absolut — normalisiert und muss genau auf die verwaltete Datei fallen,
  weil das Deployment das geschlossene Datenverzeichnis sichert. Umwege über `..`, andere Dateinamen,
  `mem:` und `tcp:` werden weiterhin abgelehnt. Zusätzlich muss eine Überschreibung
  `DB_CLOSE_DELAY=-1` mitführen, damit die Härtung nicht stillschweigend verlorengeht.
- **DB_CLOSE_ON_EXIT bleibt auf der Vorgabe TRUE.** Der Shutdown-Hook schließt die Datenbank beim
  Prozessende weiterhin, und genau darauf beruht die konsistente Sicherung des gestoppten Dienstes;
  `-1` verhindert nur das Schließen im Betrieb. Jeder weitere URL-Parameter wird abgelehnt, was
  insbesondere `DB_CLOSE_ON_EXIT=FALSE` ausschließt.

Keine Migration, kein Schemaeingriff. Die fest eingetragene URL in AdminRecovery bleibt unverändert:
dieser Offline-Befehl läuft kurzlebig ohne Pool.

- `H2ConnectionLifecycleTest`: liest die Parameter der konfigurierten Standard-URL aus
  application.properties, baut einen Pool in der Form der Produktion und zieht dessen Verbindungen
  zurück. Ohne Härtung zählt ein DATABASE_EVENT_LISTENER mindestens eine Schließung, mit der
  konfigurierten URL keine.
- `tests/operations/test_update.py`: neuer `ManagedDatabaseUrlTest` für die Pfadprüfung und die
  abgelehnten Parameter, dazu die gehärtete Überschreibung durch die vollständige Preflight.
- `JAVA_HOME=~/opt/jdk25 ./tools/ci-jvm --without-firefox` grün: BUILD SUCCESSFUL in 8m 15s,
  72 Betriebstests.

Details: `docs/implementation/h2-connection-lifecycle.md`.

Die Ursache selbst. Der fertige Commit auf Branch `failure-diagnostics` ergänzt die Logzeile um die
Kette der Exception-Klassen samt sqlState und errorCode und wird getrennt ausgeliefert; er ist das
Mittel, die Ursache beim nächsten Auftreten zu belegen statt zu raten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mi force-pushed h2-close-delay from a2aa123228 to 9db0d57bad 2026-10-23 13:38:46 +02:00 Compare
All checks were successful
werkator/branch build succeeded after 06:20
Required
Details
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin h2-close-delay:h2-close-delay
git checkout h2-close-delay
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mi/werkjournal#13