fix(seed): prevent duplicate seeding under multi-worker gunicorn
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:
+19
@@ -4,13 +4,22 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from filelock import FileLock
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app import crud, models, schemas
|
from app import crud, models, schemas
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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 = [
|
DEMO_BOOKS = [
|
||||||
{
|
{
|
||||||
"title": "Der Name des Windes",
|
"title": "Der Name des Windes",
|
||||||
@@ -121,6 +130,16 @@ DEMO_SERIES = [
|
|||||||
|
|
||||||
|
|
||||||
def seed_if_empty(db: Session) -> None:
|
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:
|
if db.query(models.Media).count() > 0:
|
||||||
return
|
return
|
||||||
log.info("Leere DB – lege Demo-Daten an.")
|
log.info("Leere DB – lege Demo-Daten an.")
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ pydantic==2.9.2
|
|||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
python-multipart==0.0.10
|
python-multipart==0.0.10
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
|
filelock==3.16.1
|
||||||
|
|||||||
Reference in New Issue
Block a user