In der FAQ von SQLite steht:
Multiple processes can have the same database open at the same time. Multiple processes can be doing a SELECT at the same time. But only one process can be making changes to the database at any moment in time, however.
Genau in dieser Situation war ich, und genau das ging nicht. In meinem Szenario läuft puma im Clustermodus, das heißt da gibt es mehrere Prozesse. Alle davon lesen die gleiche Datenbank. Zusätzlich gibt es einen schreibenden Prozess, der die Datenbank aktualisiert. Dank --preload
wird der configure
-Block der Sinatra-Anwendung nur einmal ausgeführt, dort startet die Aktualisierung in einem Thread. Also ist es genau wie in der FAQ beschrieben, ein schreibender Prozess, mehrere lesende. Doch es hagelte Fehler:
Error storing data: database is locked could not get data: database is locked
Probiere es selbst aus: Starte mit diesem Ruby-Skript einen Prozess, der in eine Datenbank schreibt:
require 'sqlite3' db = SQLite3::Database.new "test.db" db.execute "CREATE TABLE IF NOT EXISTS test(test INTEGER);" while true db.execute "INSERT INTO test VALUES(1);" db.execute "UPDATE test SET test = 2;" sleep 1 end
Und jetzt starte ein oder mehrere Prozesse, die diese Datenbank lesen:
require 'sqlite3' db = SQLite3::Database.new "test.db" while true p db.execute "SELECT * FROM test;" sleep 0.5 end
Und auch bei dir wird das sterben:
/home/onli/.rvm/gems/ruby-2.5.3/gems/sqlite3-1.3.13/lib/sqlite3/statement.rb:108:in `step': database is locked (SQLite3::BusyException)
In der FAQ kann doch nicht gemeint sein, dass mehrere Prozesse auf die Datenbank zugreifen können, solange sie haufenweise manuell solche Exceptions fangen und gescheiterte Zugriffe nochmal probieren? Was stimmt da nicht?
Des Rätsels Lösung: Der FAQ-Eintrag ist unvollständig.
SQLite kann tatsächlich über mehrere Prozesse hinweg Datenbankzugriffe teilen, und einer dieser Prozesse darf auch schreiben. Aber nicht in der Standardkonfiguration. Damit sich SQLite verhält wie beschrieben muss Write-Ahead Logging (WAL) aktiviert werden.
Wenn WAL aktiviert ist werden Änderungen nicht mehr direkt in die Datenbankdatei geschrieben, sondern erstmal in eine temporäre Datei. Ab und an wird dann synchronisiert. Es ist dieses Schema, das mehrere parallele Zugriffe über Prozessgrenzen hinweg ermöglicht.
Aber WAL ist standardmäßig aus. Um es zu aktivieren muss der Modus per PRAGMA aktiviert werden:
PRAGMA journal_mode=WAL;
Das ist persistent! Während andere Pragmas beim jeden Öffnen der Datenbank neu gesetzt werden müssen bleibt WAL aktiviert.
Und richtig: Die Testskripts von oben sterben nicht mehr, meine Anwendung beschwerte sich bisher nicht mehr über die gesperrte Datenbank.
Das ist wohl leider nicht 100%. In der Doku werden drei Szenarien beschrieben, in denen trotz WAL die Datenbank gesperrt sein kann:
- Wenn ein Prozess
PRAGMA locking_mode = EXCLUSIVE;
setzt. Simpel zu lösen: Mach das für den normalen Betrieb einfach nicht. - Wenn die letzte Verbindung (=Connection) zu einer Datenbank schließt und die WAL-Datei abgearbeitet wird. Das ist der Fall der Sorgen bereiten sollte, denn je nach Anwendung kann das regelmäßig passieren. https://sqlite.org/pragma.html#pragma_busy_timeout sollte dann eine Lösung sein.
- Wenn die letzte Verbindung zu einer Datenbank abstürzt und die nächste Verbindung die Datenbank wiederherstellt. Das sollte bei einer stabilen Anwendung nicht passieren und ist daher vermeidbar.
Sieht so aus, als könnte SQLite doch auch in diesem Szenario die Datenbank der Wahl bleiben. Andere Datenbanken mit einem Client/Servermodell haben hier zwar einen klaren Vorteil: Parallele Zugriffe sind gar kein Problem, auch mehrere schreibende nicht. Aber mit WAL und busy_timeout kann SQLite doch auch dieses Szenarien abdecken, und damit eines mehr als ursprünglich erwartet.
Netz - Rettung - Recht am : Wellenreiten 02/2020
Vorschau anzeigen