"""In-memory typed event bus for instance lifecycle and health events.""" import asyncio import inspect import logging import uuid from collections.abc import Awaitable, Callable from typing import Any logger = logging.getLogger(__name__) InstanceEventPayload = dict[str, Any] EventCallback = Callable[[InstanceEventPayload], Awaitable[None] | None] # noqa: UP044 class InstanceEventBus: """Singleton in-memory event bus with typed pub/sub and exception isolation.""" _instance: "InstanceEventBus | None" = None _lock: asyncio.Lock = asyncio.Lock() def __init__(self) -> None: self._subscribers: dict[str, list[tuple[str, EventCallback]]] = {} def __new__(cls) -> "InstanceEventBus": if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._subscribers = {} return cls._instance def _reset_for_testing(self) -> None: """Clear all subscribers. For test use only.""" self._subscribers.clear() def subscribe( self, event_type: str, callback: EventCallback, ) -> Callable[[], None]: """Register a callback for an event type. Args: event_type: The event type to subscribe to. callback: A sync or async callable that receives the payload. Returns: An unsubscribe function. """ if event_type not in self._subscribers: self._subscribers[event_type] = [] callback_id = str(uuid.uuid4()) self._subscribers[event_type].append((callback_id, callback)) def unsubscribe() -> None: self.unsubscribe(event_type, callback_id) return unsubscribe def unsubscribe(self, event_type: str, callback_id: str) -> None: """Remove a specific callback by ID.""" if event_type in self._subscribers: self._subscribers[event_type] = [ (cid, cb) for cid, cb in self._subscribers[event_type] if cid != callback_id ] if not self._subscribers[event_type]: del self._subscribers[event_type] def unsubscribe_all(self, event_type: str) -> None: """Remove all subscribers for an event type.""" self._subscribers.pop(event_type, None) async def publish(self, event_type: str, payload: InstanceEventPayload) -> None: """Deliver payload to all subscribers of event_type. Also delivers to subscribers registered under the wildcard "*". Exceptions from individual subscribers are caught and logged; delivery continues to remaining subscribers. """ callbacks: list[tuple[str, EventCallback]] = [] callbacks.extend(self._subscribers.get(event_type, [])) callbacks.extend(self._subscribers.get("*", [])) for _callback_id, callback in callbacks: try: if inspect.iscoroutinefunction(callback): await callback(payload) else: callback(payload) except Exception: correlation_id = payload.get("correlation_id", "unknown") logger.exception( "Event subscriber failed for %s", event_type, extra={"correlation_id": correlation_id}, )