Orphan-fájl auto-resume pattern#
A tanulság#
Ha a backend in-memory job-state-ben tartja az async upload-pipeline állapotát (FastAPI BackgroundTasks, asyncio.create_task, Celery in-process worker), bármilyen restart elveszíti az in-flight feladatokat. A felhasználói tünet: "a feltöltés sorbanállva marad / nem sikerült".
Megoldás (self-healing): backend-startup-kor scan-elni a data/uploads/ (vagy bárhol az input-fájlok mennek) könyvtárat, kiszedni a DB-ből az ismert (processed) fájlokat, a difference = orphan → háttér task-ban újra-ingest. Filesystem = source-of-truth, DB = derived state.
Why#
User uploads PDF → backend saves file + registers job in _jobs dict + BackgroundTasks
↓
Backend restart (code deploy, OOM, OS reboot, crash)
↓
_jobs dict üres (in-memory)
↓
UI poll /api/jobs/{id} → 404 forever
↓
File saved to disk de soha nem processzeolt → DB-ben nincs catalog
A felhasználó látja a "queued" status-t hosszan, miközben a backend már nem dolgozik rajta semmin.
How to apply#
FastAPI startup-handler:
# backend/app/startup.py
import asyncio
import logging
from pathlib import Path
from backend.app.db.database import get_connection
from backend.pipeline.ingest import ingest_pdf
logger = logging.getLogger(__name__)
def _find_orphan_pdfs() -> list[Path]:
"""PDF-ek a data/pdfs/-ben amiknek nincs catalog-rekordjuk."""
pdfs = sorted(Path("data/pdfs").glob("*.pdf"))
with get_connection() as conn:
rows = conn.execute("SELECT source_pdf FROM catalogs").fetchall()
known = {Path(r["source_pdf"]).name for r in rows}
return [p for p in pdfs if p.name not in known]
async def _auto_resume_orphans():
orphans = _find_orphan_pdfs()
if not orphans:
return
logger.warning("Auto-resume: %d orphan PDF, ingest indul", len(orphans))
for pdf in orphans:
# Blocking ingest egy thread-en — ne blokkolja az event-loop-ot
await asyncio.to_thread(ingest_pdf, pdf, reset=False)
def schedule_auto_resume(app):
@app.on_event("startup")
async def _startup():
asyncio.create_task(_auto_resume_orphans())
Hozzákapcsolás:
# backend/app/main.py
from backend.app.startup import schedule_auto_resume
app = FastAPI(...)
schedule_auto_resume(app)
Frontend fallback (nem helyettesíti, csak kiegészíti):
A frontend a job_id-t pollozza, ha 404 érkezik → fallback /api/catalogs listához és matchel filename-stem-mel. Ha találja → treat as done.
Mikor kell ez a pattern#
- ✅ Bármilyen pipeline ahol a feldolgozás >5s, és restartok happen (dev, deploy, OOM)
- ✅ Input-fájl-alapú workflow (a fájl = source-of-truth)
- ✅ FastAPI BackgroundTasks single-worker (in-process state)
- ❌ NEM kell ha:
- Redis-backed job-queue (Celery/Sidekiq) — already persistent
- Idempotent endpoints amik nem hagynak köztes állapotot
- Stream-processing (Kafka, etc.)
Architektúra-elv#
Filesystem = source-of-truth, DB = derived state.
Ennek implikációi: - DB DROP után rebuild lehet a filesystem-ből - Backend restart NEM veszít adatot, csak átmeneti workflow-állapotot - Idempotens re-ingest (új catalog row, NEM update) — minden re-run egy "next try" - Disk-térrel fizetjük: input-fájlokat sosem töröljük (csak ha a user expliciten kéri)
Tovább-fejlesztés#
Ha a self-healing-en túl lépünk: - SQLite job-table ahelyett hogy in-memory dict — DB-ben tárolt status, frontend pontosan látja a "recovering 3/5" üzenetet - Idempotency-key a POST /api/upload-ra → ugyanaz a key = ugyanaz a job_id, deduplication - Worker pool (uvicorn workers>1 vagy Celery + Redis) — több párhuzamos ingest
Implementáció#
- 02-Projects/robbantott-kereso
backend/app/startup.py(commitc302b3d) - Felhasználói "megint nem sikerül feltölteni" → diagnosztikus session-ből kiderült: 4 orphan PDF + auto-resume → 95% recovery probléma nélkül
Kapcsolódó#
- Karpathy-LLM-Wiki-pattern (sem=0.51)
- puppeteer-pdf-system-chrome (sem=0.49)