Files
wehub-resource-sync 6ede33ccdb
Build and Push Docker Images / create_manifest (web, surfsense-web, , cpu) (push) Has been cancelled
Build and Push Docker Images / finalize_release (push) Has been cancelled
Obsidian Plugin Lint / lint (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / compute_version (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / verify_digests (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, , cpu) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda, cuda) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda126, cuda126) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:33:44 +08:00

98 lines
3.6 KiB
Python

"""In-process pub/sub bus for streaming a scraper run's progress over SSE.
One process, one bus (the module-level :data:`run_event_bus`). Per ``run_id`` it
holds three things:
* a set of subscriber ``asyncio.Queue`` s — one per open SSE connection;
* a bounded ring buffer of recent events so a late/reconnecting subscriber can
replay what it missed before tailing live;
* the background ``asyncio.Task`` running the scrape, so the cancel endpoint can
reach it.
State for a run is dropped when it reaches a terminal event (``run.finished``);
a client that connects *after* that reads the final snapshot from the ``runs``
row instead. ``ponytail:`` single-process only — a multi-worker deployment needs
Redis pub/sub (or Postgres LISTEN/NOTIFY) behind this same interface.
"""
from __future__ import annotations
import asyncio
import logging
from collections import deque
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from collections.abc import Iterable
logger = logging.getLogger(__name__)
_BUFFER_SIZE = 500
_SUBSCRIBER_QUEUE_SIZE = 1000
class RunEventBus:
"""Fan-out of per-run progress events to live SSE subscribers."""
def __init__(self, *, buffer_size: int = _BUFFER_SIZE) -> None:
self._subscribers: dict[str, set[asyncio.Queue[dict[str, Any]]]] = {}
self._buffers: dict[str, deque[dict[str, Any]]] = {}
self._tasks: dict[str, asyncio.Task[Any]] = {}
self._buffer_size = buffer_size
# -- task registry (for cancellation) --------------------------------
def register_task(self, run_id: str, task: asyncio.Task[Any]) -> None:
self._tasks[run_id] = task
def get_task(self, run_id: str) -> asyncio.Task[Any] | None:
return self._tasks.get(run_id)
# -- publish / subscribe ---------------------------------------------
def publish(self, run_id: str, event: dict[str, Any]) -> None:
"""Buffer an event and fan it out to every live subscriber.
Called from within the event loop by the running scrape. Full
subscriber queues drop the event rather than block the scrape;
the replay buffer still preserves recent history.
"""
buffer = self._buffers.setdefault(run_id, deque(maxlen=self._buffer_size))
buffer.append(event)
for queue in list(self._subscribers.get(run_id, ())):
try:
queue.put_nowait(event)
except asyncio.QueueFull:
logger.debug("run %s: subscriber queue full, dropping event", run_id)
def subscribe(self, run_id: str) -> asyncio.Queue[dict[str, Any]]:
queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(
maxsize=_SUBSCRIBER_QUEUE_SIZE
)
self._subscribers.setdefault(run_id, set()).add(queue)
return queue
def unsubscribe(self, run_id: str, queue: asyncio.Queue[dict[str, Any]]) -> None:
subscribers = self._subscribers.get(run_id)
if subscribers is None:
return
subscribers.discard(queue)
if not subscribers:
self._subscribers.pop(run_id, None)
def replay(self, run_id: str) -> Iterable[dict[str, Any]]:
return list(self._buffers.get(run_id, ()))
def close(self, run_id: str) -> None:
"""Drop all state for a finished run.
Terminal events are published *before* this, so any live subscriber has
already received ``run.finished`` in its queue and will break its loop.
"""
self._buffers.pop(run_id, None)
self._subscribers.pop(run_id, None)
self._tasks.pop(run_id, None)
run_event_bus = RunEventBus()