base_agent/uas/event/event_bus.py

421 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
事件总线核心
─────────────────────────────────────────────────────────────────
基于 PyQt5 QObject/pyqtSignal + QThreadPool 实现:
- 线程安全的事件提交(同步 / 异步)
- 按优先级排序的处理器链
- 支持同步槽 & 异步协程处理器
- 事件历史记录
- 统计信息
─────────────────────────────────────────────────────────────────
"""
from __future__ import annotations
import asyncio
import logging
import threading
import time
from collections import defaultdict, deque
from typing import Callable, Dict, List, Optional, Union
from PyQt5.QtCore import (
QObject, QRunnable, QThreadPool,
pyqtSignal, pyqtSlot, QTimer
)
from uas.event.event_handler import (
BaseEventHandler, HandlerDescriptor,
get_decorator_handlers
)
from uas.event.event_model import Event, EventResult, EventState
from uas.event.event_type import EventType
logger = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────────
# PyQt5 信号载体
# ─────────────────────────────────────────────────────────────────
class _BusSignals(QObject):
"""事件总线信号集合"""
# 事件提交信号event 对象)
event_submitted = pyqtSignal(object)
# 事件处理完成信号event 对象)
event_handled = pyqtSignal(object)
# 事件处理失败信号event 对象, error_msg
event_failed = pyqtSignal(object, str)
# 事件被取消信号
event_cancelled = pyqtSignal(object)
# 无处理器信号
event_ignored = pyqtSignal(object)
# 统计更新信号total, handled, failed
stats_updated = pyqtSignal(int, int, int)
# ─────────────────────────────────────────────────────────────────
# 事件处理 Worker在 QThreadPool 工作线程中执行)
# ─────────────────────────────────────────────────────────────────
class _EventWorkerSignals(QObject):
finished = pyqtSignal(object) # Event
failed = pyqtSignal(object, str)
class _EventWorker(QRunnable):
"""
在 QThreadPool 工作线程中执行处理器链
每个 Event 对应一个独立 Worker
"""
def __init__(
self,
event: Event,
descriptors: List[HandlerDescriptor],
):
super().__init__()
self.event = event
self.descriptors = descriptors
self.signals = _EventWorkerSignals()
self.setAutoDelete(True)
@pyqtSlot()
def run(self):
event = self.event
event.mark_processing()
try:
for desc in self.descriptors:
# 检查事件是否已被取消
if event.is_cancelled:
logger.info(f"事件已取消,跳过后续处理器: {event}")
break
# 条件过滤
if not desc.matches(event):
continue
t0 = time.perf_counter()
try:
if desc.is_async:
# 异步处理器:在新事件循环中运行
result_data = asyncio.run(desc.fn(event))
else:
result_data = desc.fn(event)
elapsed = (time.perf_counter() - t0) * 1000
desc.call_count += 1
event.add_result(EventResult(
handler_name = desc.name,
success = True,
data = result_data,
elapsed_ms = elapsed,
))
logger.debug(
f"处理器 [{desc.name}] 完成 "
f"event={event.event_id[:8]} elapsed={elapsed:.1f}ms"
)
# 一次性处理器标记
if desc.once:
desc.fn = lambda e: None # 失效
except Exception as e:
elapsed = (time.perf_counter() - t0) * 1000
err_msg = f"{type(e).__name__}: {e}"
event.add_result(EventResult(
handler_name = desc.name,
success = False,
error = err_msg,
elapsed_ms = elapsed,
))
logger.error(
f"处理器 [{desc.name}] 异常: {err_msg}", exc_info=True
)
event.mark_done()
self.signals.finished.emit(event)
except Exception as e:
event.state = EventState.FAILED
self.signals.failed.emit(event, str(e))
logger.error(f"Worker 严重异常: {e}", exc_info=True)
# ─────────────────────────────────────────────────────────────────
# 事件总线主类
# ─────────────────────────────────────────────────────────────────
class EventBus(QObject):
"""
事件总线(单例)
─────────────────────────────────────────────────────────────
使用方式:
bus = EventBus.instance()
# 方式1注册类处理器
bus.register_handler(DeviceErrorHandler())
# 方式2注册函数处理器
bus.subscribe(EventType.DEVICE_ERROR, my_handler_fn)
# 方式3@on_event 装饰器(自动扫描)
bus.scan_decorators()
# 提交事件(异步,不阻塞主线程)
bus.post(make_event(EventType.DEVICE_ERROR, source="sensor-01"))
# 同步提交(阻塞等待所有处理器完成)
bus.post_sync(event)
─────────────────────────────────────────────────────────────
"""
_instance = None
_lock = threading.Lock()
# 对外暴露的信号
event_submitted = pyqtSignal(object)
event_handled = pyqtSignal(object)
event_failed = pyqtSignal(object, str)
event_cancelled = pyqtSignal(object)
event_ignored = pyqtSignal(object)
stats_updated = pyqtSignal(int, int, int)
@classmethod
def instance(cls) -> "EventBus":
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self, max_threads: int = 8, history_size: int = 500, parent=None):
super().__init__(parent)
self._pool: QThreadPool = QThreadPool.globalInstance()
self._pool.setMaxThreadCount(max_threads)
# { event_type_key: [HandlerDescriptor, ...] }(按 priority 排序)
self._handlers: Dict[str, List[HandlerDescriptor]] = defaultdict(list)
# 通配符处理器
self._wildcard_handlers: List[HandlerDescriptor] = []
# 事件历史(循环队列)
self._history: deque[Event] = deque(maxlen=history_size)
self._history_lock = threading.Lock()
# 统计
self._stats = {"total": 0, "handled": 0, "failed": 0, "ignored": 0}
self._stats_lock = threading.Lock()
logger.info(f"✅ EventBus 初始化 max_threads={max_threads}")
# ── 处理器注册 ────────────────────────────────────────────────
def register_handler(self, handler: BaseEventHandler) -> None:
"""注册类处理器(继承 BaseEventHandler"""
desc = handler.descriptor
self._add_descriptor(desc)
logger.info(f"注册类处理器: {desc}")
def subscribe(
self,
event_type: Union[EventType, str],
fn: Callable,
priority: int = 0,
condition: Optional[Callable[[Event], bool]] = None,
once: bool = False,
) -> HandlerDescriptor:
"""
注册函数处理器
:return: HandlerDescriptor可用于后续 unsubscribe
"""
desc = HandlerDescriptor(
fn = fn,
event_types = [event_type],
priority = priority,
condition = condition,
once = once,
name = fn.__name__,
)
self._add_descriptor(desc)
logger.info(f"注册函数处理器: {desc}")
return desc
def unsubscribe(self, descriptor: HandlerDescriptor) -> None:
"""注销处理器"""
for key in descriptor.event_types:
if key in self._handlers:
self._handlers[key] = [
d for d in self._handlers[key] if d is not descriptor
]
if descriptor in self._wildcard_handlers:
self._wildcard_handlers.remove(descriptor)
logger.info(f"注销处理器: {descriptor.name}")
def scan_decorators(self) -> int:
"""扫描并注册所有 @on_event 装饰的处理器"""
handlers = get_decorator_handlers()
for desc in handlers:
self._add_descriptor(desc)
logger.info(f"扫描到 {len(handlers)} 个 @on_event 处理器")
return len(handlers)
def _add_descriptor(self, desc: HandlerDescriptor):
if "*" in desc.event_types:
self._wildcard_handlers.append(desc)
self._wildcard_handlers.sort(key=lambda d: d.priority)
else:
for key in desc.event_types:
self._handlers[key].append(desc)
self._handlers[key].sort(key=lambda d: d.priority)
# ── 事件提交 ──────────────────────────────────────────────────
def post(self, event: Event) -> None:
"""
异步提交事件(不阻塞调用线程)
事件在 QThreadPool 工作线程中处理
"""
event.mark_pending()
self._update_stats("total")
self._add_history(event)
self.event_submitted.emit(event)
descriptors = self._get_descriptors(event)
if not descriptors:
event.state = EventState.IGNORED
self._update_stats("ignored")
self.event_ignored.emit(event)
logger.debug(f"无处理器,忽略事件: {event}")
return
worker = _EventWorker(event, descriptors)
worker.signals.finished.connect(self._on_worker_finished)
worker.signals.failed.connect(self._on_worker_failed)
self._pool.start(worker)
logger.debug(f"异步提交事件: {event} handlers={len(descriptors)}")
def post_sync(self, event: Event, timeout_ms: int = 5000) -> Event:
"""
同步提交事件(阻塞等待所有处理器完成)
:param timeout_ms: 超时时间(毫秒)
:return: 处理完成的 Event 对象
"""
event.mark_pending()
self._update_stats("total")
self._add_history(event)
descriptors = self._get_descriptors(event)
if not descriptors:
event.state = EventState.IGNORED
self._update_stats("ignored")
return event
# 同步执行处理器链
event.mark_processing()
for desc in descriptors:
if event.is_cancelled:
break
if not desc.matches(event):
continue
t0 = time.perf_counter()
try:
if desc.is_async:
result_data = asyncio.run(desc.fn(event))
else:
result_data = desc.fn(event)
elapsed = (time.perf_counter() - t0) * 1000
event.add_result(EventResult(
handler_name=desc.name, success=True,
data=result_data, elapsed_ms=elapsed
))
except Exception as e:
elapsed = (time.perf_counter() - t0) * 1000
event.add_result(EventResult(
handler_name=desc.name, success=False,
error=str(e), elapsed_ms=elapsed
))
event.mark_done()
self._update_stats("handled" if event.state == EventState.HANDLED else "failed")
return event
def post_delayed(self, event: Event, delay_ms: int) -> None:
"""延迟提交事件"""
QTimer.singleShot(delay_ms, lambda: self.post(event))
logger.debug(f"延迟 {delay_ms}ms 提交事件: {event}")
# ── 槽函数 ────────────────────────────────────────────────────
@pyqtSlot(object)
def _on_worker_finished(self, event: object):
if event.state == EventState.CANCELLED:
self._update_stats("ignored")
self.event_cancelled.emit(event)
elif event.state == EventState.FAILED:
self._update_stats("failed")
self.event_failed.emit(event, event.results[-1].error if event.results else "")
else:
self._update_stats("handled")
self.event_handled.emit(event)
s = self._stats
self.stats_updated.emit(s["total"], s["handled"], s["failed"])
logger.debug(f"事件完成: {event}")
@pyqtSlot(object, str)
def _on_worker_failed(self, event: object, error: str):
self._update_stats("failed")
self.event_failed.emit(event, error)
logger.error(f"事件处理严重失败: {event} error={error}")
# ── 内部工具 ──────────────────────────────────────────────────
def _get_descriptors(self, event: Event) -> List[HandlerDescriptor]:
"""获取匹配事件的所有处理器,按优先级合并排序"""
specific = self._handlers.get(event.type_key, [])
combined = specific + self._wildcard_handlers
combined.sort(key=lambda d: d.priority)
return combined
def _add_history(self, event: Event):
with self._history_lock:
self._history.append(event)
def _update_stats(self, key: str):
with self._stats_lock:
self._stats[key] = self._stats.get(key, 0) + 1
# ── 查询接口 ──────────────────────────────────────────────────
def get_history(
self,
event_type: Optional[Union[EventType, str]] = None,
limit: int = 50,
) -> List[Event]:
"""查询事件历史"""
with self._history_lock:
history = list(self._history)
if event_type:
key = event_type.value if isinstance(event_type, EventType) else event_type
history = [e for e in history if e.type_key == key]
return history[-limit:]
def get_stats(self) -> dict:
with self._stats_lock:
return dict(self._stats)
def clear_history(self):
with self._history_lock:
self._history.clear()
def wait_for_done(self, timeout_ms: int = 5000):
"""等待所有 Worker 完成"""
self._pool.waitForDone(timeout_ms)
# 全局总线实例
event_bus = EventBus.instance()