"""In-process bus transports. * :class:`FakeBus` — real pub/sub semantics without touching a socket. Used by unit tests and anywhere ``DECNET_BUS_TYPE=fake`` is set. Lets code that depends on the bus be exercised entirely inside a single event loop, matching the DECNET testing convention of not opening real network sockets from unit tests. * :class:`NullBus` — no-op. Returned by :func:`~decnet.bus.factory.get_bus` when ``DECNET_BUS_ENABLED=false`` so workers can start cleanly in dev environments where no bus daemon is running. Publishes are dropped; subscriptions yield nothing and close cleanly. """ from __future__ import annotations import asyncio from typing import Any from decnet.bus.base import ( BaseBus, Event, Subscription, _CLOSE_SENTINEL, matches, ) from decnet.logging import get_logger log = get_logger("bus.fake") # Per-subscriber bounded queue: backpressure policy is drop-oldest so a slow # consumer cannot stall publishers (the invariant — DB is the source of # truth — makes dropped events acceptable). _DEFAULT_QUEUE_SIZE = 1024 # ─── FakeBus ───────────────────────────────────────────────────────────────── class _FakeSubscription(Subscription): """Subscription backed by an :class:`asyncio.Queue` fed from :meth:`FakeBus.publish`. Unregisters itself on close.""" def __init__(self, bus: "FakeBus", pattern: str, queue: "asyncio.Queue[Any]") -> None: super().__init__(pattern) self._bus = bus self._queue = queue async def __anext__(self) -> Event: if self._closed: raise StopAsyncIteration item = await self._queue.get() if item is _CLOSE_SENTINEL: raise StopAsyncIteration return item async def _aclose(self) -> None: self._bus._unregister(self) # Unblock any pending __anext__ waiter. try: self._queue.put_nowait(_CLOSE_SENTINEL) except asyncio.QueueFull: pass class FakeBus(BaseBus): """In-process pub/sub. Publishes iterate every active subscription and enqueue the event on the ones whose pattern matches the topic. If a subscriber's queue is full, the oldest event is discarded to make room — same at-most-once semantics as the real UNIX-socket transport. """ def __init__(self, queue_size: int = _DEFAULT_QUEUE_SIZE) -> None: self._queue_size = queue_size self._subs: list[_FakeSubscription] = [] self._connected = False self._closed = False self._lock = asyncio.Lock() async def connect(self) -> None: self._connected = True async def publish( self, topic: str, payload: dict[str, Any], *, event_type: str = "", ) -> None: if self._closed: raise RuntimeError("publish on closed bus") event = Event(topic=topic, payload=payload, type=event_type) async with self._lock: targets = [s for s in self._subs if matches(s.pattern, topic)] for sub in targets: _enqueue_drop_oldest(sub._queue, event) def subscribe(self, pattern: str) -> Subscription: if self._closed: raise RuntimeError("subscribe on closed bus") queue: asyncio.Queue[Any] = asyncio.Queue(maxsize=self._queue_size) sub = _FakeSubscription(self, pattern, queue) self._subs.append(sub) return sub def _unregister(self, sub: _FakeSubscription) -> None: try: self._subs.remove(sub) except ValueError: pass async def close(self) -> None: if self._closed: return self._closed = True # Wake every still-open subscription so iterators unblock cleanly. for sub in list(self._subs): try: sub._queue.put_nowait(_CLOSE_SENTINEL) except asyncio.QueueFull: pass self._subs.clear() def _enqueue_drop_oldest(queue: "asyncio.Queue[Any]", event: Event) -> None: """Put *event* on *queue*, dropping the oldest item if the queue is full. Factored out so both FakeBus and the real UNIX server share the exact same backpressure policy. """ while True: try: queue.put_nowait(event) return except asyncio.QueueFull: try: dropped = queue.get_nowait() log.warning( "bus.fake: subscriber queue full, dropped %s", getattr(dropped, "topic", "?") ) except asyncio.QueueEmpty: return # ─── NullBus ───────────────────────────────────────────────────────────────── class _NullSubscription(Subscription): """A subscription that never yields and closes immediately on iteration.""" async def __anext__(self) -> Event: raise StopAsyncIteration async def _aclose(self) -> None: return class NullBus(BaseBus): """No-op bus used when ``DECNET_BUS_ENABLED=false``. Publishes are silently dropped; subscriptions are empty. Intended for dev environments where no bus daemon is running — the process starts cleanly, code that publishes doesn't need feature flags, and nothing ever blocks on a subscriber. """ async def connect(self) -> None: return async def publish( self, topic: str, payload: dict[str, Any], *, event_type: str = "", ) -> None: return def subscribe(self, pattern: str) -> Subscription: return _NullSubscription(pattern) async def close(self) -> None: return