Files
yawan-workbench/server/app/progress.py
T

95 lines
3.0 KiB
Python

"""Cross-process progress event bus.
The legacy bus was process-local, which worked when FastAPI spawned content
generation threads. With an external DSH worker, events are persisted in
SQLite and replayed by SSE/polling; the in-memory deque remains a fast path
for events emitted inside the API process.
"""
from __future__ import annotations
import logging
import threading
import time
from collections import deque
from typing import Any
logger = logging.getLogger("yawen.progress")
_LOCK = threading.Lock()
_QUEUES: dict[int, deque] = {}
_MAX_PER_USER = 200
def publish(user_id: int, event: str, **fields: Any) -> None:
item = {"event": event, "ts": time.time(), **fields}
uid = int(user_id or 0)
with _LOCK:
targets = list(_QUEUES.keys()) if uid == 0 else [uid]
for target in targets:
q = _QUEUES.get(target)
if q is not None:
q.append(item)
while len(q) > _MAX_PER_USER:
q.popleft()
try:
from sqlmodel import Session
from .db import engine
from .models import ProgressEvent
with Session(engine) as session:
session.add(ProgressEvent(user_id=uid, event=str(event)[:80], payload=item))
session.commit()
except Exception as exc: # progress must never block production
logger.debug("persist progress event failed: %s", exc)
def register(user_id: int) -> deque:
with _LOCK:
q = _QUEUES.get(int(user_id))
if q is None:
q = deque(maxlen=_MAX_PER_USER)
_QUEUES[int(user_id)] = q
return q
def unregister(user_id: int) -> None:
with _LOCK:
_QUEUES.pop(int(user_id), None)
def list_events(user_id: int, after_ts: float = 0.0, limit: int = 200) -> list[dict]:
uid = int(user_id or 0)
events: list[dict] = []
try:
from sqlmodel import Session, select
from .db import engine
from .models import ProgressEvent
with Session(engine) as session:
rows = session.exec(
select(ProgressEvent)
.where(ProgressEvent.user_id.in_([0, uid]))
.order_by(ProgressEvent.id.desc())
.limit(500)
).all()
events = [dict(row.payload or {}) for row in reversed(rows)]
except Exception:
events = []
with _LOCK:
q = _QUEUES.get(uid)
if q is not None:
events.extend(list(q))
result: list[dict] = []
seen: set[tuple] = set()
for event in events:
if float(event.get("ts") or 0.0) <= float(after_ts or 0.0):
continue
key = (event.get("event"), event.get("ts"), event.get("campaign_id"),
event.get("segment_id"), event.get("title"))
if key in seen:
continue
seen.add(key)
result.append(event)
return result[-limit:]
def snapshot(user_id: int, after_ts: float = 0.0, limit: int = 200) -> list[dict]:
return list_events(user_id, after_ts, limit=limit)