fix(scheduler): prevent duplicate slot assignment from concurrent pipeline runs
Two bugs caused multiple articles to land on the same publish slot: 1. main.py: asyncio.create_task() returned immediately, allowing a second pipeline trigger (N8N + Telegram /run or two N8N calls) to start a second concurrent run. Added asyncio.Lock (_pipeline_lock) so any second trigger while the pipeline is running is rejected immediately. 2. scheduler.py: reserve_publish_slot() read the list of occupied slots and wrote the new slot in two separate DB connections. Concurrent threads could both see the same "free" slot before either committed its write. Fixed by wrapping the entire read-find-write cycle in a threading.Lock (_slot_lock) and a single DB connection, so the slot check and the slot assignment are atomic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2456e4aca7
commit
f710141828
2 changed files with 90 additions and 35 deletions
|
|
@ -1,8 +1,10 @@
|
|||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
import csv
|
||||
from datetime import datetime, timezone
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, Response, status
|
||||
|
|
@ -637,20 +639,26 @@ def _require_api_key(request: Request) -> None:
|
|||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Ungültiger API-Key")
|
||||
|
||||
|
||||
_pipeline_lock = asyncio.Lock()
|
||||
|
||||
|
||||
@app.post("/api/n8n/pipeline")
|
||||
async def api_n8n_pipeline(request: Request) -> dict:
|
||||
"""Trigger the full auto pipeline in background. Returns immediately.
|
||||
Called by N8N (2x/day or on demand). Results arrive via Telegram."""
|
||||
_require_api_key(request)
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
if _pipeline_lock.locked():
|
||||
logging.getLogger(__name__).warning("Pipeline bereits aktiv – Trigger ignoriert")
|
||||
return {"ok": False, "message": "Pipeline läuft bereits – Trigger ignoriert"}
|
||||
|
||||
async def _run():
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
await loop.run_in_executor(None, lambda: run_auto_pipeline(trigger="n8n"))
|
||||
except Exception as exc:
|
||||
logging.getLogger(__name__).error("Background pipeline error: %s", exc)
|
||||
async with _pipeline_lock:
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
await loop.run_in_executor(None, lambda: run_auto_pipeline(trigger="n8n"))
|
||||
except Exception as exc:
|
||||
logging.getLogger(__name__).error("Background pipeline error: %s", exc)
|
||||
|
||||
asyncio.create_task(_run())
|
||||
return {"ok": True, "message": "Pipeline gestartet – Ergebnisse kommen per Telegram"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue