95 lines
3.0 KiB
Python
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)
|