Files
MoviePilot/app/runtime/event/dispatch.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

436 lines
16 KiB
Python
Raw Permalink 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.
"""
链式和广播事件的独立调度算法。
"""
from __future__ import annotations
import inspect
import time
from collections.abc import Callable
from contextlib import AbstractContextManager, nullcontext
from typing import Any
from app.runtime.correlation import correlation_scope
from app.runtime.event.binding import EventBindingResolver
from app.runtime.event.registry import EventRegistry
from app.runtime.execution import run_in_threadpool
from app.runtime.log import bind_plugin_instance, logger
from app.runtime.observability import observe_duration
from app.schemas.types import EventType
def _instance_binding_scope(class_name: str) -> AbstractContextManager[None]:
"""
处理器归属某个类时绑定其插件实例日志上下文,自由函数处理器不绑定。
``class_name`` 就是声明该处理器的类的 ``__name__``;宿主类处理器同样会
命中这里,但缓存里没有它们的等级覆盖记录,过滤时自然回落全局等级。
"""
return bind_plugin_instance(class_name) if class_name else nullcontext()
class EventDispatcher:
"""
基于订阅快照执行链式或广播事件,不拥有注册和生命周期状态。
"""
def __init__(
self,
*,
registry: EventRegistry,
binding_resolver: EventBindingResolver,
event_factory: Callable[..., Any],
error_handler: Callable[..., None],
async_handle_sink: Callable[[Any], bool],
sync_handle_sink: Callable[
[Callable[..., Any], tuple[Any, ...]],
bool,
],
) -> None:
"""
注入注册表、绑定器、生命周期提交器和错误策略回调。
"""
self._registry = registry
self._binding_resolver = binding_resolver
self._event_factory = event_factory
self._error_handler = error_handler
self._async_handle_sink = async_handle_sink
self._sync_handle_sink = sync_handle_sink
def _begin_chain_dispatch(
self, event: Any
) -> tuple[tuple[str, int, Callable[..., Any]], ...]:
"""
冻结启用处理器快照,并统一空快照与开始生命周期记录。
"""
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 ()
self._log_lifecycle(event, "Started")
return enabled
@staticmethod
def _log_chain_handler_completed(
handler: Callable[..., Any],
priority: int,
started_at: float,
event: Any,
) -> None:
"""
统一记录同步与异步处理器完成耗时和优先级。
"""
logger.debug(
"%s (Priority: %s), completed in %.3fs for event: %s",
EventRegistry.handler_identifier(handler),
priority,
time.time() - started_at,
event,
)
def _complete_chain_dispatch(self, event: Any) -> bool:
"""
统一提交链式事件完成生命周期并返回已处理标识。
"""
self._log_lifecycle(event, "Completed")
return True
def dispatch_chain(self, event: Any) -> bool:
"""
同步按优先级顺序执行链式事件快照。
"""
enabled = self._begin_chain_dispatch(event)
if not enabled:
return False
for _handler_id, priority, handler in enabled:
started_at = time.time()
self.invoke_sync(handler, event)
EventDispatcher._log_chain_handler_completed(
handler, priority, started_at, event
)
return self._complete_chain_dispatch(event)
async def async_dispatch_chain(self, event: Any) -> bool:
"""
异步按优先级顺序执行链式事件快照。
"""
enabled = self._begin_chain_dispatch(event)
if not enabled:
return False
for _handler_id, priority, handler in enabled:
started_at = time.time()
await self.invoke_async(handler, event)
EventDispatcher._log_chain_handler_completed(
handler, priority, started_at, event
)
return self._complete_chain_dispatch(event)
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,
correlation_id=event.correlation_id,
)
if inspect.iscoroutinefunction(handler):
coroutine = self.safe_invoke_async(handler, isolated)
self._async_handle_sink(coroutine)
else:
self._sync_handle_sink(
self.safe_invoke_sync,
(handler, isolated),
)
def dispatch_broadcast_strict(
self,
event: Any,
async_runner: Callable[[Any], Any],
) -> None:
"""
串行执行广播处理器并等待完成,任一处理失败时向调用方抛出。
"""
handlers = self._registry.broadcast_snapshot(event.event_type)
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 not self._registry.is_handler_enabled(handler):
continue
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,
correlation_id=event.correlation_id,
)
if inspect.iscoroutinefunction(handler):
async_runner(self.invoke_async_strict(handler, isolated))
else:
self.invoke_sync_strict(handler, isolated)
async def dispatch_broadcast_async_strict(self, event: Any) -> None:
"""
异步串行等待广播处理器完成,供实时配置重载入口使用。
"""
handlers = self._registry.broadcast_snapshot(event.event_type)
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 not self._registry.is_handler_enabled(handler):
continue
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,
correlation_id=event.correlation_id,
)
if inspect.iscoroutinefunction(handler):
await self.invoke_async_strict(
handler,
isolated,
skip_unresolved=True,
run_sync_in_threadpool=True,
)
else:
# 复用统一严格调用路径,保持 owner 的线程池声明和错误策略一致。
await self.invoke_async_strict(
handler,
isolated,
skip_unresolved=True,
run_sync_in_threadpool=True,
)
def safe_invoke_sync(self, handler: Callable[..., object], event: Any) -> None:
"""
仅在处理器启用时执行同步调用。
"""
if self._registry.is_handler_enabled(handler):
self.invoke_sync(handler, event)
async def safe_invoke_async(self, handler: Callable[..., object], event: Any) -> None:
"""
仅在处理器启用时执行异步调用。
"""
if self._registry.is_handler_enabled(handler):
await self.invoke_async(handler, event)
def invoke_sync(self, handler: Callable[..., object], event: Any) -> None:
"""
解析实例绑定并同步调用处理器。
"""
resolved = self._binding_resolver.resolve(handler)
if not resolved:
return
method, binding, class_name, method_name = resolved
with correlation_scope(event.correlation_id):
try:
with observe_duration(
"event.handler.duration",
event_type=event.event_type.value,
handler_type="bound" if class_name else "function",
), _instance_binding_scope(class_name):
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,
)
def invoke_sync_strict(
self,
handler: Callable[..., object],
event: Any,
*,
skip_unresolved: bool = False,
) -> None:
"""
解析并执行同步处理器,按需跳过未激活 owner,实际错误向调用方传播。
"""
resolved = self._binding_resolver.resolve(handler)
if not resolved:
if skip_unresolved:
return
raise RuntimeError("事件处理器实例不可用")
method, binding, class_name, method_name = resolved
with correlation_scope(event.correlation_id):
try:
with observe_duration(
"event.handler.duration",
event_type=event.event_type.value,
handler_type="bound" if class_name else "function",
), _instance_binding_scope(class_name):
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,
)
raise
async def invoke_async(self, handler: Callable[..., object], event: Any) -> None:
"""
解析实例绑定,并按处理器类型选择协程、线程池或同步调用。
"""
resolved = self._binding_resolver.resolve(handler)
if not resolved:
return
method, binding, class_name, method_name = resolved
with correlation_scope(event.correlation_id):
try:
with observe_duration(
"event.handler.duration",
event_type=event.event_type.value,
handler_type="bound" if class_name else "function",
), _instance_binding_scope(class_name):
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,
)
async def invoke_async_strict(
self,
handler: Callable[..., object],
event: Any,
*,
skip_unresolved: bool = False,
run_sync_in_threadpool: bool = False,
) -> None:
"""
解析并等待处理器完成,按需跳过未激活 owner 或移交同步处理器线程池。
"""
resolved = self._binding_resolver.resolve(handler)
if not resolved:
if skip_unresolved:
return
raise RuntimeError("事件处理器实例不可用")
method, binding, class_name, method_name = resolved
with correlation_scope(event.correlation_id):
try:
with observe_duration(
"event.handler.duration",
event_type=event.event_type.value,
handler_type="bound" if class_name else "function",
), _instance_binding_scope(class_name):
if inspect.iscoroutinefunction(method):
await method(event)
elif run_sync_in_threadpool or 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,
)
raise
@staticmethod
def should_dispatch_to_target_plugin(
handler: Callable[..., object],
handler_identifier: str,
target_plugin_id: str,
) -> bool:
"""
只把定向输入事件投递给标识和声明均匹配的目标插件。
目标匹配按运行实例身份判断,不能只看处理器的限定名:分身共享源码,其
``__qualname__`` 保持源类名不变,只比类名会让定向到分身的事件全部落空,
而定向到本体的事件被本体连同它的全部分身一起收到。实例身份编码在处理器
标识的模块段里(``app.plugins.<实例ID>``);不是插件处理器时回落到类名比较,
保持宿主侧处理器的既有行为。
"""
class_name, method_name = EventBindingResolver.parse_handler_names(handler)
parts = (handler_identifier or "").split(".")
if len(parts) < 2 or parts[-2:] != [class_name, method_name]:
return False
module_path = ".".join(parts[:-2])
prefix = "app.plugins."
if module_path.startswith(prefix):
owner_id = module_path[len(prefix):].split(".")[0]
return owner_id.casefold() == target_plugin_id.casefold()
return class_name == target_plugin_id
@staticmethod
def _log_lifecycle(event: Any, stage: str) -> None:
"""
记录事件调度的开始和完成阶段。
"""
logger.debug("%s - %s", stage, event)