116 lines
4.5 KiB
Python
116 lines
4.5 KiB
Python
"""一炁进程内事件订阅分发器。
|
||
|
||
订阅 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,
|
||
)
|