feat(pubsub): EventBus with drop-oldest overflow, per-kind fan-out
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""In-process pub/sub for live-tail event streaming.
|
||||
|
||||
``EventBus`` is a tiny, async-first fan-out broker. Publishers call
|
||||
``publish(kind, payload)`` and never block; if a subscriber's per-kind
|
||||
queue is full, the oldest event is dropped so the slow consumer cannot
|
||||
stall the producer. Subscribers receive events through an async
|
||||
iterator that yields ``{**payload, "_kind": kind}``.
|
||||
|
||||
The bus is intentionally not thread-safe: it is designed to run on a
|
||||
single asyncio event loop, which matches the FastAPI/uvicorn
|
||||
deployment model used elsewhere in this project.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
|
||||
class EventBus:
|
||||
"""Per-kind fan-out bus with drop-oldest overflow per subscriber."""
|
||||
|
||||
def __init__(self, max_queue_size: int = 256) -> None:
|
||||
self._max_queue_size = max_queue_size
|
||||
self._subscribers: dict[str, list[asyncio.Queue[dict]]] = {}
|
||||
|
||||
async def publish(self, kind: str, payload: dict) -> None:
|
||||
"""Fan ``payload`` out to every subscriber of ``kind``.
|
||||
|
||||
Never blocks. If a subscriber queue is full, the oldest queued
|
||||
event is discarded to make room.
|
||||
"""
|
||||
event = {**payload, "_kind": kind}
|
||||
for queue in list(self._subscribers.get(kind, ())):
|
||||
self._enqueue_or_drop_oldest(queue, event)
|
||||
|
||||
def _enqueue_or_drop_oldest(
|
||||
self, queue: asyncio.Queue[dict], event: dict
|
||||
) -> None:
|
||||
try:
|
||||
queue.put_nowait(event)
|
||||
except asyncio.QueueFull:
|
||||
# Drop the oldest event to make room, then enqueue.
|
||||
queue.get_nowait()
|
||||
queue.task_done()
|
||||
queue.put_nowait(event)
|
||||
|
||||
def subscribe(self, kinds: list[str]) -> AsyncIterator[dict]:
|
||||
"""Return an async iterator delivering events for any of ``kinds``."""
|
||||
queue: asyncio.Queue[dict] = asyncio.Queue(maxsize=self._max_queue_size)
|
||||
for kind in kinds:
|
||||
self._subscribers.setdefault(kind, []).append(queue)
|
||||
return self._iterator(queue)
|
||||
|
||||
async def _iterator(self, queue: asyncio.Queue[dict]) -> AsyncIterator[dict]:
|
||||
while True:
|
||||
yield await queue.get()
|
||||
queue.task_done()
|
||||
Reference in New Issue
Block a user