fix(seed): prevent duplicate seeding under multi-worker gunicorn
CI / test (push) Has been cancelled
CI / docker (push) Has been cancelled

When the beta image runs with --workers > 1 (default: 2), every worker
calls lifespan() in parallel. Each worker sees an empty DB, then
seed_if_empty() runs concurrently, producing 2x (or Nx) demo entries.

Fix: wrap the seed operation in a process-wide FileLock. Even if a
worker loses the race it re-checks the table count inside the critical
section and returns without re-seeding.

Verified by cold-starting a 4-worker container: 8 items (was: 32).
This commit is contained in:
ki
2026-07-21 23:40:57 +02:00
parent 0bca2eaaa2
commit ccf90d3e46
2 changed files with 25 additions and 5 deletions
+19
View File
@@ -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,6 +130,16 @@ 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
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.")
+1
View File
@@ -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