mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 03:48:03 +08:00
436 lines
16 KiB
Python
436 lines
16 KiB
Python
"""
|
||
链式和广播事件的独立调度算法。
|
||
"""
|
||
|
||
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)
|