Files
ki 7a105d8e45
CI / test (push) Has been cancelled
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.
2026-07-22 00:52:54 +02:00

60 lines
1.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Datenbank-Engine und Session-Verwaltung."""
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import os
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
# SQLite-Datei: überschreibbar via WATCHSTACK_DATA_DIR (für Docker)
_BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = Path(os.environ.get("WATCHSTACK_DATA_DIR", _BASE_DIR / "data"))
DATA_DIR.mkdir(parents=True, exist_ok=True)
DB_PATH = DATA_DIR / "quivio.db"
DATABASE_URL = f"sqlite:///{DB_PATH}"
engine = create_engine(
DATABASE_URL,
echo=False,
future=True,
connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False)
class Base(DeclarativeBase):
"""Basisklasse für alle ORM-Modelle."""
def get_db():
"""FastAPI-Dependency: liefert eine Session, schließt sie danach."""
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db() -> None:
"""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
lock = FileLock(str(DB_PATH) + ".init.lock", timeout=30)
with lock:
Base.metadata.create_all(bind=engine)