初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
"""一炁进程内事件订阅分发器。
订阅 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,
)