diff --git a/app/seed.py b/app/seed.py index cc762bf..da2193d 100644 --- a/app/seed.py +++ b/app/seed.py @@ -4,13 +4,22 @@ from __future__ import annotations import logging from datetime import date +from pathlib import Path +from filelock import FileLock from sqlalchemy.orm import Session from app import crud, models, schemas log = logging.getLogger(__name__) + +# File-Lock gegen Race-Conditions bei mehreren Workern (gunicorn). +# Pro Datenbank-Pfad ein Lock, damit parallele Worker nicht doppelt seeden. +def _lock_path() -> Path: + from app.database import DB_PATH + return DB_PATH.with_suffix(DB_PATH.suffix + ".seed.lock") + DEMO_BOOKS = [ { "title": "Der Name des Windes", @@ -121,10 +130,20 @@ DEMO_SERIES = [ def seed_if_empty(db: Session) -> None: + """Seedet genau einmal pro DB. + + Mehrere gunicorn-Worker können gleichzeitig hochfahren und alle eine leere DB + sehen. Ein File-Lock serialisiert das Seeding prozessübergreifend. + """ if db.query(models.Media).count() > 0: return - log.info("Leere DB – lege Demo-Daten an.") - for spec in DEMO_BOOKS: - crud.create_media(db, schemas.MediaCreate(kind="book", **spec)) - for spec in DEMO_SERIES: - crud.create_media(db, schemas.MediaCreate(kind="series", **spec)) + lock = FileLock(str(_lock_path()), timeout=30) + with lock: + # Re-check unter Lock: ein anderer Worker hat womoeglich schon geseedet. + if db.query(models.Media).count() > 0: + return + log.info("Leere DB – lege Demo-Daten an.") + for spec in DEMO_BOOKS: + crud.create_media(db, schemas.MediaCreate(kind="book", **spec)) + for spec in DEMO_SERIES: + crud.create_media(db, schemas.MediaCreate(kind="series", **spec)) diff --git a/requirements.txt b/requirements.txt index 59ca07a..c45c28d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pydantic==2.9.2 httpx==0.27.2 python-multipart==0.0.10 jinja2==3.1.4 +filelock==3.16.1