"""一炁进程内事件订阅分发器。 订阅 EventStore 全量事件(``subscribe_by_pattern("*")``),按 glob pattern 分发给注册的 handler。每个 ``(pattern, handler)`` 注册项 = 一个独立 subscription(独立 queue + consumer task),handler 异常隔离(``try/except`` + ``logger.exception``),at-most-once(失败不重试)。 设计见 ``docs/architecture/data-and-security-architecture.md`` §17「进程内订阅范式」。 """ from __future__ import annotations import asyncio import contextlib import fnmatch from collections.abc import Awaitable, Callable from dataclasses import dataclass, field from loguru import logger from app.modules.runtime.events import Event, EventStore AsyncHandler = Callable[[Event], Awaitable[None]] @dataclass class _Subscription: pattern: str handler: AsyncHandler queue: asyncio.Queue[Event] = field(default_factory=asyncio.Queue) class Dispatcher: """一炁事件订阅分发器。 用法: dispatcher = Dispatcher(event_store) dispatcher.register("time.*", handler) # 必须在 start 前注册 await dispatcher.start() # lifespan startup ... await dispatcher.stop() # lifespan shutdown """ def __init__(self, event_store: EventStore) -> None: self._event_store = event_store self._subscriptions: list[_Subscription] = [] self._all_queue: asyncio.Queue[Event] | None = None self._pump_task: asyncio.Task[None] | None = None self._consumer_tasks: list[asyncio.Task[None]] = [] self._started = False def register(self, pattern: str, handler: AsyncHandler) -> None: """注册 handler(必须在 start 前调用)。 pattern: glob 如 ``'time.reunion'`` / ``'time.*'`` / ``'*'``(fnmatch 语义)。 同一 handler 注册多 pattern = 多个独立 subscription(v1 不去重)。 """ if self._started: raise RuntimeError( "Dispatcher 已启动,不能注册 handler(应在 lifespan startup 前注册)" ) self._subscriptions.append(_Subscription(pattern=pattern, handler=handler)) async def start(self) -> None: """lifespan startup:subscribe_by_pattern('*') + 起 pump task + 各 subscription consumer task。""" if self._started: return self._all_queue = self._event_store.subscribe_by_pattern("*") self._pump_task = asyncio.create_task(self._pump(), name="dispatcher-pump") for sub in self._subscriptions: task = asyncio.create_task( self._run_consumer(sub), name=f"dispatcher-consumer-{sub.pattern}" ) self._consumer_tasks.append(task) self._started = True logger.info("Dispatcher 启动:{} 个 subscription", len(self._subscriptions)) async def stop(self) -> None: """lifespan shutdown:cancel 所有 task + await 取消 + unsubscribe all_queue。""" if not self._started: return tasks = [t for t in [self._pump_task, *self._consumer_tasks] if t is not None] for task in tasks: task.cancel() for task in tasks: with contextlib.suppress(asyncio.CancelledError): await task if self._all_queue is not None: self._event_store.unsubscribe_by_pattern("*", self._all_queue) self._pump_task = None self._consumer_tasks = [] self._all_queue = None self._started = False logger.info("Dispatcher 停止") async def _pump(self) -> None: """读 all_queue → glob 匹配 subscription → put 到 per-subscription queue(无 IO,不抛)。""" assert self._all_queue is not None while True: event = await self._all_queue.get() for sub in self._subscriptions: if fnmatch.fnmatch(event.type, sub.pattern): sub.queue.put_nowait(event) async def _run_consumer(self, sub: _Subscription) -> None: """per-subscription consumer:读 queue → try/except 调 handler(at-most-once 隔离)。""" while True: event = await sub.queue.get() try: await sub.handler(event) except Exception: logger.exception( "Dispatcher handler 异常(at-most-once,跳过): pattern={} type={}", sub.pattern, event.type, )