fix(db): lock init_db() to prevent "table already exists" race
CI / test (push) Has been cancelled

Beim Cold-Start eines Containers mit persistentem Volume (DB ist nicht
leer) liefen die gunicorn-Worker parallel in init_db() und beide
versuchten CREATE TABLE. Einer crashed mit 'table genres already exists',
der andere startete.

Symmetrisch zum seed-Lock: init_db() jetzt auch in FileLock gewrapped.
Zusaetzlich *.init.lock in .gitignore.

Verifiziert mit 2 Workern + bestehender DB: beide Container starten
sauber, kein Crash, 8 items erhalten.
This commit is contained in:
ki
2026-07-22 00:52:54 +02:00
parent 9a4909f083
commit 7a105d8e45
2 changed files with 16 additions and 2 deletions
+1
View File
@@ -40,6 +40,7 @@ logs/
# File-Lock-Artefakte (Race-Condition-Schutz) # File-Lock-Artefakte (Race-Condition-Schutz)
*.seed.lock *.seed.lock
*.init.lock
# dist/ ist ein lokales Build-Ausgabeverzeichnis. Docker-Images werden # dist/ ist ein lokales Build-Ausgabeverzeichnis. Docker-Images werden
# in die Gitea Container Registry gepusht (bin/build-and-push.sh), nicht # in die Gitea Container Registry gepusht (bin/build-and-push.sh), nicht
+14 -1
View File
@@ -40,7 +40,20 @@ def get_db():
def init_db() -> None: def init_db() -> None:
"""Erstellt alle Tabellen, falls noch nicht vorhanden.""" """Erstellt alle Tabellen, falls noch nicht vorhanden.
Bei mehreren Workern (gunicorn) starten alle parallel und rufen
``init_db()`` auf. Ohne Lock rufen beide ``Base.metadata.create_all()``
parallel auf, was bei SQLite zu 'table X already exists' fuehren kann.
Ein File-Lock serialisiert das prozessuebergreifend.
Mit ``checkfirst=True`` (Default seit SQLAlchemy 1.4) wuerde SQLAlchemy
zwar pruefen, ob die Tabelle existiert — aber das ist selbst ein SELECT
und kann zwischen den Workern race-anfaellig sein. Lock ist robuster.
"""
from filelock import FileLock
from quivio import models # noqa: F401 registriert Modelle bei Base.metadata from quivio import models # noqa: F401 registriert Modelle bei Base.metadata
lock = FileLock(str(DB_PATH) + ".init.lock", timeout=30)
with lock:
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)