From 7a105d8e453de17c3e1f480291ce068349624021 Mon Sep 17 00:00:00 2001 From: ki Date: Wed, 22 Jul 2026 00:52:54 +0200 Subject: [PATCH] fix(db): lock init_db() to prevent "table already exists" race 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. --- .gitignore | 1 + quivio/database.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 2920123..b4a93f9 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ logs/ # File-Lock-Artefakte (Race-Condition-Schutz) *.seed.lock +*.init.lock # dist/ ist ein lokales Build-Ausgabeverzeichnis. Docker-Images werden # in die Gitea Container Registry gepusht (bin/build-and-push.sh), nicht diff --git a/quivio/database.py b/quivio/database.py index b162c64..d7a8e42 100644 --- a/quivio/database.py +++ b/quivio/database.py @@ -40,7 +40,20 @@ def get_db(): 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 - Base.metadata.create_all(bind=engine) + lock = FileLock(str(DB_PATH) + ".init.lock", timeout=30) + with lock: + Base.metadata.create_all(bind=engine)