mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 16:36:53 +08:00
refactor: 推进后端分层架构治理
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""事件运行时内部组件。"""
|
||||
@@ -0,0 +1,138 @@
|
||||
"""事件处理器声明到运行实例的显式绑定解析。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
from app.runtime.event.registry import EventRegistry
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EventHandlerBinding:
|
||||
"""描述上层运行时为某个事件处理器提供的实例绑定。"""
|
||||
|
||||
instance: Optional[Any]
|
||||
owner_name: str
|
||||
run_sync_in_threadpool: bool = False
|
||||
|
||||
|
||||
HandlerInstanceResolver = Callable[
|
||||
[Type[Any]], Optional[EventHandlerBinding]
|
||||
]
|
||||
|
||||
|
||||
class EventBindingResolver:
|
||||
"""只通过已登记 resolver 把类处理器绑定到托管运行实例。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
lock: Any,
|
||||
resolvers: Callable[[], dict[str, HandlerInstanceResolver]],
|
||||
) -> None:
|
||||
"""绑定 resolver 存储,并记录未命中的处理器用于启动诊断。"""
|
||||
self._lock = lock
|
||||
self._resolvers = resolvers
|
||||
self._unresolved: set[str] = set()
|
||||
|
||||
def register(self, name: str, resolver: HandlerInstanceResolver) -> None:
|
||||
"""注册或替换命名实例解析器。"""
|
||||
with self._lock:
|
||||
self._resolvers()[name] = resolver
|
||||
|
||||
def unresolved_handlers(self) -> tuple[str, ...]:
|
||||
"""返回本进程中未被显式 resolver 接管的类处理器。"""
|
||||
with self._lock:
|
||||
return tuple(sorted(self._unresolved))
|
||||
|
||||
@staticmethod
|
||||
def parse_handler_names(handler: Callable) -> tuple[str, str]:
|
||||
"""解析处理器限定名中的类名和方法名。"""
|
||||
names = handler.__qualname__.split(".")
|
||||
if len(names) < 2:
|
||||
return "", names[0]
|
||||
return names[0], names[1]
|
||||
|
||||
@staticmethod
|
||||
def owner_class(handler: Callable) -> Optional[Type[Any]]:
|
||||
"""从处理器对象本身解析声明类,不按字符串动态导入模块。"""
|
||||
if inspect.ismethod(handler):
|
||||
owner = handler.__self__
|
||||
return owner if isinstance(owner, type) else type(owner)
|
||||
module = inspect.getmodule(handler)
|
||||
if not module:
|
||||
return None
|
||||
owner: Any = module
|
||||
for part in handler.__qualname__.split(".")[:-1]:
|
||||
if part == "<locals>":
|
||||
return None
|
||||
owner = getattr(owner, part, None)
|
||||
if owner is None:
|
||||
return None
|
||||
return owner if isinstance(owner, type) else None
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
handler: Callable,
|
||||
) -> Optional[tuple[Callable, EventHandlerBinding, str, str]]:
|
||||
"""通过显式 resolver 解析当前实例方法;自由函数直接返回。"""
|
||||
owner_class = self.owner_class(handler)
|
||||
method_name = getattr(
|
||||
handler,
|
||||
"__name__",
|
||||
self.parse_handler_names(handler)[1],
|
||||
)
|
||||
if owner_class is None:
|
||||
binding = EventHandlerBinding(
|
||||
instance=None,
|
||||
owner_name=EventRegistry.handler_identifier(handler),
|
||||
run_sync_in_threadpool=True,
|
||||
)
|
||||
return handler, binding, "", method_name
|
||||
|
||||
with self._lock:
|
||||
resolvers = tuple(self._resolvers().items())
|
||||
binding = None
|
||||
resolver_name = ""
|
||||
for name, resolver in resolvers:
|
||||
candidate = resolver(owner_class)
|
||||
if candidate is not None:
|
||||
binding = candidate
|
||||
resolver_name = name
|
||||
break
|
||||
if binding is None:
|
||||
identifier = EventRegistry.handler_identifier(handler)
|
||||
with self._lock:
|
||||
first_miss = identifier not in self._unresolved
|
||||
self._unresolved.add(identifier)
|
||||
if first_miss:
|
||||
logger.warning(
|
||||
"事件处理器未绑定显式 resolver,已跳过:%s",
|
||||
identifier,
|
||||
)
|
||||
return None
|
||||
logger.debug(
|
||||
"事件处理器绑定:%s -> %s",
|
||||
EventRegistry.handler_identifier(handler),
|
||||
resolver_name,
|
||||
)
|
||||
if binding.instance is None:
|
||||
return None
|
||||
method = getattr(binding.instance, method_name, None)
|
||||
if not callable(method):
|
||||
fallback_name = self.parse_handler_names(handler)[1]
|
||||
method = getattr(binding.instance, fallback_name, None)
|
||||
if fallback_name == method_name or not callable(method):
|
||||
logger.warning(
|
||||
"事件处理器 %s 无法解析为实例方法 %s.%s,跳过执行",
|
||||
EventRegistry.handler_identifier(handler),
|
||||
owner_class.__name__,
|
||||
method_name,
|
||||
)
|
||||
return None
|
||||
method_name = fallback_name
|
||||
return method, binding, owner_class.__name__, method_name
|
||||
@@ -0,0 +1,196 @@
|
||||
"""链式和广播事件的独立调度算法。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from app.runtime.event.binding import EventBindingResolver
|
||||
from app.runtime.event.registry import EventRegistry
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
class EventDispatcher:
|
||||
"""基于订阅快照执行链式或广播事件,不拥有注册和生命周期状态。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
registry: EventRegistry,
|
||||
binding_resolver: EventBindingResolver,
|
||||
executor: Callable[[], Any],
|
||||
event_loop: Callable[[], Any],
|
||||
event_factory: Callable[..., Any],
|
||||
error_handler: Callable[..., None],
|
||||
) -> None:
|
||||
"""注入注册表、绑定器、执行器和错误策略回调。"""
|
||||
self._registry = registry
|
||||
self._binding_resolver = binding_resolver
|
||||
self._executor = executor
|
||||
self._event_loop = event_loop
|
||||
self._event_factory = event_factory
|
||||
self._error_handler = error_handler
|
||||
|
||||
def dispatch_chain(self, event: Any) -> bool:
|
||||
"""同步按优先级顺序执行链式事件快照。"""
|
||||
handlers = self._registry.chain_snapshot(event.event_type)
|
||||
enabled = tuple(
|
||||
(handler_id, priority, handler)
|
||||
for handler_id, (priority, handler) in handlers
|
||||
if self._registry.is_handler_enabled(handler)
|
||||
)
|
||||
if not enabled:
|
||||
logger.debug("No enabled handlers found for chain event: %s", event)
|
||||
return False
|
||||
self._log_lifecycle(event, "Started")
|
||||
for _handler_id, priority, handler in enabled:
|
||||
started_at = time.time()
|
||||
self.invoke_sync(handler, event)
|
||||
logger.debug(
|
||||
"%s (Priority: %s), completed in %.3fs for event: %s",
|
||||
EventRegistry.handler_identifier(handler),
|
||||
priority,
|
||||
time.time() - started_at,
|
||||
event,
|
||||
)
|
||||
self._log_lifecycle(event, "Completed")
|
||||
return True
|
||||
|
||||
async def async_dispatch_chain(self, event: Any) -> bool:
|
||||
"""异步按优先级顺序执行链式事件快照。"""
|
||||
handlers = self._registry.chain_snapshot(event.event_type)
|
||||
enabled = tuple(
|
||||
(handler_id, priority, handler)
|
||||
for handler_id, (priority, handler) in handlers
|
||||
if self._registry.is_handler_enabled(handler)
|
||||
)
|
||||
if not enabled:
|
||||
logger.debug("No enabled handlers found for chain event: %s", event)
|
||||
return False
|
||||
self._log_lifecycle(event, "Started")
|
||||
for _handler_id, priority, handler in enabled:
|
||||
started_at = time.time()
|
||||
await self.invoke_async(handler, event)
|
||||
logger.debug(
|
||||
"%s (Priority: %s), completed in %.3fs for event: %s",
|
||||
EventRegistry.handler_identifier(handler),
|
||||
priority,
|
||||
time.time() - started_at,
|
||||
event,
|
||||
)
|
||||
self._log_lifecycle(event, "Completed")
|
||||
return True
|
||||
|
||||
def dispatch_broadcast(self, event: Any) -> None:
|
||||
"""按订阅快照把广播事件投递到线程池或主事件循环。"""
|
||||
handlers = self._registry.broadcast_snapshot(event.event_type)
|
||||
if not handlers:
|
||||
logger.debug("No handlers found for broadcast event: %s", event)
|
||||
return
|
||||
target_plugin_id = None
|
||||
if event.event_type == EventType.MessageAction and isinstance(
|
||||
event.event_data,
|
||||
dict,
|
||||
):
|
||||
target_plugin_id = event.event_data.get("__mp_target_plugin_id")
|
||||
for handler_id, handler in handlers:
|
||||
if target_plugin_id and not self.should_dispatch_to_target_plugin(
|
||||
handler,
|
||||
handler_id,
|
||||
str(target_plugin_id),
|
||||
):
|
||||
continue
|
||||
if isinstance(event.event_data, dict):
|
||||
event_data = event.event_data.copy()
|
||||
event_data.pop("__mp_target_plugin_id", None)
|
||||
else:
|
||||
event_data = event.event_data
|
||||
isolated = self._event_factory(
|
||||
event_type=event.event_type,
|
||||
event_data=event_data,
|
||||
priority=event.priority,
|
||||
)
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.safe_invoke_async(handler, isolated),
|
||||
self._event_loop(),
|
||||
)
|
||||
else:
|
||||
self._executor().submit(
|
||||
self.safe_invoke_sync,
|
||||
handler,
|
||||
isolated,
|
||||
)
|
||||
|
||||
def safe_invoke_sync(self, handler: Callable, event: Any) -> None:
|
||||
"""仅在处理器启用时执行同步调用。"""
|
||||
if self._registry.is_handler_enabled(handler):
|
||||
self.invoke_sync(handler, event)
|
||||
|
||||
async def safe_invoke_async(self, handler: Callable, event: Any) -> None:
|
||||
"""仅在处理器启用时执行异步调用。"""
|
||||
if self._registry.is_handler_enabled(handler):
|
||||
await self.invoke_async(handler, event)
|
||||
|
||||
def invoke_sync(self, handler: Callable, event: Any) -> None:
|
||||
"""解析实例绑定并同步调用处理器。"""
|
||||
resolved = self._binding_resolver.resolve(handler)
|
||||
if not resolved:
|
||||
return
|
||||
method, binding, class_name, method_name = resolved
|
||||
try:
|
||||
method(event)
|
||||
except Exception as err:
|
||||
self._error_handler(
|
||||
event=event,
|
||||
module_name=binding.owner_name,
|
||||
class_name=class_name,
|
||||
method_name=method_name,
|
||||
e=err,
|
||||
)
|
||||
|
||||
async def invoke_async(self, handler: Callable, event: Any) -> None:
|
||||
"""解析实例绑定,并按处理器类型选择协程、线程池或同步调用。"""
|
||||
resolved = self._binding_resolver.resolve(handler)
|
||||
if not resolved:
|
||||
return
|
||||
method, binding, class_name, method_name = resolved
|
||||
try:
|
||||
if inspect.iscoroutinefunction(method):
|
||||
await method(event)
|
||||
elif binding.run_sync_in_threadpool or not class_name:
|
||||
await run_in_threadpool(method, event)
|
||||
else:
|
||||
method(event)
|
||||
except Exception as err:
|
||||
self._error_handler(
|
||||
event=event,
|
||||
module_name=binding.owner_name,
|
||||
class_name=class_name,
|
||||
method_name=method_name,
|
||||
e=err,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def should_dispatch_to_target_plugin(
|
||||
handler: Callable,
|
||||
handler_identifier: str,
|
||||
target_plugin_id: str,
|
||||
) -> bool:
|
||||
"""只把定向输入事件投递给标识和声明均匹配的目标插件。"""
|
||||
class_name, method_name = EventBindingResolver.parse_handler_names(handler)
|
||||
if class_name != target_plugin_id:
|
||||
return False
|
||||
parts = (handler_identifier or "").split(".")
|
||||
return len(parts) >= 2 and parts[-2:] == [class_name, method_name]
|
||||
|
||||
@staticmethod
|
||||
def _log_lifecycle(event: Any, stage: str) -> None:
|
||||
"""记录事件调度的开始和完成阶段。"""
|
||||
logger.debug("%s - %s", stage, event)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""事件处理异常的通知、降级和递归保护策略。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
EventErrorNotifier = Callable[[str, str], object]
|
||||
|
||||
|
||||
class EventErrorPolicy:
|
||||
"""隔离处理器异常,并阻止 SystemError 处理失败再次广播。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
notifier: Callable[[], Optional[EventErrorNotifier]],
|
||||
emit_system_error: Callable[[dict], object],
|
||||
) -> None:
|
||||
"""注入通知读取器和 SystemError 发送回调。"""
|
||||
self._notifier = notifier
|
||||
self._emit_system_error = emit_system_error
|
||||
|
||||
def handle(
|
||||
self,
|
||||
*,
|
||||
event: Any,
|
||||
module_name: str,
|
||||
class_name: str,
|
||||
method_name: str,
|
||||
error: Exception,
|
||||
) -> None:
|
||||
"""记录并通知异常;SystemError 自身失败时只降级写日志。"""
|
||||
trace = traceback.format_exc()
|
||||
logger.error("%s 事件处理出错:%s - %s", module_name, str(error), trace)
|
||||
notifier = self._notifier()
|
||||
if notifier:
|
||||
try:
|
||||
notifier(
|
||||
f"{module_name} 处理事件 {event.event_type} 时出错",
|
||||
f"{class_name}.{method_name}:{str(error)}",
|
||||
)
|
||||
except Exception as notify_error:
|
||||
logger.error("发送事件错误通知失败:%s", str(notify_error))
|
||||
if event.event_type == EventType.SystemError:
|
||||
logger.error(
|
||||
"SystemError 处理器再次失败,停止错误事件递归广播:%s.%s",
|
||||
class_name,
|
||||
method_name,
|
||||
)
|
||||
return
|
||||
self._emit_system_error(
|
||||
{
|
||||
"type": "event",
|
||||
"event_type": event.event_type,
|
||||
"event_handle": f"{class_name}.{method_name}",
|
||||
"error": str(error),
|
||||
"traceback": trace,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,200 @@
|
||||
"""事件订阅、禁用状态和调度快照注册表。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import ChainEventType, EventType
|
||||
|
||||
|
||||
class EventRegistry:
|
||||
"""集中管理事件处理器注册、启停和不可变调度快照。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
lock: Any,
|
||||
broadcast_subscribers: Callable[[], dict],
|
||||
chain_subscribers: Callable[[], dict],
|
||||
disabled_handlers: Callable[[], set],
|
||||
disabled_classes: Callable[[], set],
|
||||
) -> None:
|
||||
"""绑定由兼容门面持有的存储,便于热重载和旧测试替换快照。"""
|
||||
self._lock = lock
|
||||
self._broadcast_subscribers = broadcast_subscribers
|
||||
self._chain_subscribers = chain_subscribers
|
||||
self._disabled_handlers = disabled_handlers
|
||||
self._disabled_classes = disabled_classes
|
||||
|
||||
@staticmethod
|
||||
def handler_identifier(target: Callable | type) -> str:
|
||||
"""返回包含模块和限定名的稳定处理器标识。"""
|
||||
module = inspect.getmodule(target)
|
||||
module_name = module.__name__ if module else "unknown_module"
|
||||
return f"{module_name}.{target.__qualname__}"
|
||||
|
||||
@classmethod
|
||||
def handler_class_identifier(cls, handler: Callable) -> str | None:
|
||||
"""返回可调用对象所属类的稳定标识;自由函数返回空值。"""
|
||||
if inspect.ismethod(handler) and hasattr(handler, "__self__"):
|
||||
return cls.handler_identifier(handler.__self__.__class__)
|
||||
if not inspect.isfunction(handler) and hasattr(handler, "__call__"):
|
||||
return cls.handler_identifier(handler.__class__)
|
||||
qualname_parts = handler.__qualname__.split(".")
|
||||
if len(qualname_parts) <= 1:
|
||||
return None
|
||||
module = inspect.getmodule(handler)
|
||||
module_name = module.__name__ if module else "unknown_module"
|
||||
return f"{module_name}.{'.'.join(qualname_parts[:-1])}"
|
||||
|
||||
def is_handler_enabled(self, handler: Callable) -> bool:
|
||||
"""判断处理器及其所属类是否均处于启用状态。"""
|
||||
handler_id = self.handler_identifier(handler)
|
||||
class_id = self.handler_class_identifier(handler)
|
||||
return not (
|
||||
handler_id in self._disabled_handlers()
|
||||
or (
|
||||
class_id is not None
|
||||
and class_id in self._disabled_classes()
|
||||
)
|
||||
)
|
||||
|
||||
def check(self, event_type: EventType | ChainEventType) -> bool:
|
||||
"""检查指定事件是否存在启用的处理器。"""
|
||||
if isinstance(event_type, ChainEventType):
|
||||
handlers = self._chain_subscribers().get(event_type, {})
|
||||
return any(
|
||||
self.is_handler_enabled(handler)
|
||||
for _, handler in handlers.values()
|
||||
)
|
||||
handlers = self._broadcast_subscribers().get(event_type, {})
|
||||
return any(self.is_handler_enabled(handler) for handler in handlers.values())
|
||||
|
||||
def add(
|
||||
self,
|
||||
event_type: EventType | ChainEventType,
|
||||
handler: Callable,
|
||||
priority: int,
|
||||
) -> None:
|
||||
"""注册处理器,并为链式事件按优先级维护稳定顺序。"""
|
||||
with self._lock:
|
||||
handler_id = self.handler_identifier(handler)
|
||||
if isinstance(event_type, ChainEventType):
|
||||
subscribers = self._chain_subscribers()
|
||||
handlers = subscribers.setdefault(event_type, {})
|
||||
existed = handler_id in handlers
|
||||
handlers.pop(handler_id, None)
|
||||
if not existed:
|
||||
logger.debug(
|
||||
"Subscribed to chain event: %s, Priority: %s - %s",
|
||||
event_type.value,
|
||||
priority,
|
||||
handler_id,
|
||||
)
|
||||
handlers[handler_id] = (priority, handler)
|
||||
subscribers[event_type] = dict(
|
||||
sorted(handlers.items(), key=lambda item: item[1][0])
|
||||
)
|
||||
return
|
||||
subscribers = self._broadcast_subscribers()
|
||||
handlers = subscribers.setdefault(event_type, {})
|
||||
existed = handler_id in handlers
|
||||
handlers.pop(handler_id, None)
|
||||
if not existed:
|
||||
logger.debug(
|
||||
"Subscribed to broadcast event: %s - %s",
|
||||
event_type.value,
|
||||
handler_id,
|
||||
)
|
||||
handlers[handler_id] = handler
|
||||
|
||||
def remove(
|
||||
self,
|
||||
event_type: EventType | ChainEventType,
|
||||
handler: Callable,
|
||||
) -> None:
|
||||
"""从指定事件中移除处理器。"""
|
||||
with self._lock:
|
||||
handler_id = self.handler_identifier(handler)
|
||||
if isinstance(event_type, ChainEventType):
|
||||
self._chain_subscribers().get(event_type, {}).pop(
|
||||
handler_id,
|
||||
None,
|
||||
)
|
||||
logger.debug(
|
||||
"Unsubscribed from chain event: %s - %s",
|
||||
event_type.value,
|
||||
handler_id,
|
||||
)
|
||||
return
|
||||
self._broadcast_subscribers().get(event_type, {}).pop(
|
||||
handler_id,
|
||||
None,
|
||||
)
|
||||
logger.debug(
|
||||
"Unsubscribed from broadcast event: %s - %s",
|
||||
event_type.value,
|
||||
handler_id,
|
||||
)
|
||||
|
||||
def disable(self, target: Callable | type) -> None:
|
||||
"""禁用单个处理器或整个处理器类。"""
|
||||
identifier = self.handler_identifier(target)
|
||||
if isinstance(target, type):
|
||||
self._disabled_classes().add(identifier)
|
||||
logger.debug("Disabled event handler class - %s", identifier)
|
||||
else:
|
||||
self._disabled_handlers().add(identifier)
|
||||
logger.debug("Disabled event handler - %s", identifier)
|
||||
|
||||
def enable(self, target: Callable | type) -> None:
|
||||
"""重新启用单个处理器或整个处理器类。"""
|
||||
identifier = self.handler_identifier(target)
|
||||
if isinstance(target, type):
|
||||
self._disabled_classes().discard(identifier)
|
||||
logger.debug("Enabled event handler class - %s", identifier)
|
||||
else:
|
||||
self._disabled_handlers().discard(identifier)
|
||||
logger.debug("Enabled event handler - %s", identifier)
|
||||
|
||||
def chain_snapshot(self, event_type: ChainEventType) -> tuple:
|
||||
"""返回当前链式订阅快照,运行期变更从下一次事件生效。"""
|
||||
with self._lock:
|
||||
return tuple(self._chain_subscribers().get(event_type, {}).items())
|
||||
|
||||
def broadcast_snapshot(self, event_type: EventType) -> tuple:
|
||||
"""返回当前广播订阅快照,运行期变更从下一次事件生效。"""
|
||||
with self._lock:
|
||||
return tuple(
|
||||
self._broadcast_subscribers().get(event_type, {}).items()
|
||||
)
|
||||
|
||||
def visualize(self) -> list[dict]:
|
||||
"""导出所有订阅处理器的事件、优先级和启停状态。"""
|
||||
result = []
|
||||
combined = {
|
||||
**self._broadcast_subscribers(),
|
||||
**self._chain_subscribers(),
|
||||
}
|
||||
for event_type, subscribers in combined.items():
|
||||
for handler_id, handler_data in subscribers.items():
|
||||
if isinstance(handler_data, tuple) and len(handler_data) == 2:
|
||||
priority, handler = handler_data
|
||||
else:
|
||||
priority, handler = None, handler_data
|
||||
item = {
|
||||
"event_type": event_type.value,
|
||||
"handler_identifier": handler_id,
|
||||
"status": (
|
||||
"enabled"
|
||||
if self.is_handler_enabled(handler)
|
||||
else "disabled"
|
||||
),
|
||||
}
|
||||
if priority is not None:
|
||||
item["priority"] = priority
|
||||
result.append(item)
|
||||
return result
|
||||
Reference in New Issue
Block a user