Files
MoviePilot/app/runtime/extensions/plugin/gate.py

90 lines
3.1 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.
"""
插件生命周期的互斥执行权和慢钩子诊断,仅在更新所有权时短暂持锁。
"""
import threading
import time
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Optional
from app.schemas.exception import PluginMutationRejectedError
from app.schemas.plugin import PluginLifecycleActivity
@dataclass(frozen=True, slots=True)
class _Operation:
"""
同一执行线程内嵌套的生命周期操作或钩子。
:param operation: 操作名称
:param plugin_id: 当前处理的插件 ID;全局准备阶段为空
:param hook: 当前钩子或加载阶段;未进入时为空
:param started_at: 单调时钟记录的进入时刻,单位为秒
"""
operation: str
plugin_id: Optional[str]
hook: Optional[str]
started_at: float
class PluginLifecycleGate:
"""
保留共享注册表的串行约束,竞争者立即拒绝,不持锁运行第三方代码。
"""
def __init__(self) -> None:
"""
初始化执行线程归属与嵌套操作栈。
"""
self._lock = threading.RLock()
self._thread_id: Optional[int] = None
self._operations: list[_Operation] = []
@contextmanager
def hold(
self, operation: str, plugin_id: Optional[str] = None, hook: Optional[str] = None,
) -> Iterator[None]:
"""
为当前线程取得执行权,允许同步停启嵌套,其他线程快速失败。
:raises PluginMutationRejectedError: 另一个线程仍持有真实执行权
"""
current_thread = threading.get_ident()
with self._lock:
if self._operations and self._thread_id != current_thread:
activity = self.activity()
assert activity is not None
raise PluginMutationRejectedError(
operation,
f"插件生命周期正忙:{activity.plugin_id or '全部插件'} "
f"{activity.operation}/{activity.hook or '准备'} "
f"已执行 {activity.elapsed_seconds:.2f} 秒",
)
self._thread_id = current_thread
self._operations.append(_Operation(operation, plugin_id, hook, time.monotonic()))
try:
yield
finally:
with self._lock:
self._operations.pop()
if not self._operations:
self._thread_id = None
def activity(self) -> Optional[PluginLifecycleActivity]:
"""
返回当前真实执行者及钩子耗时,不等待其第三方代码完成。
"""
with self._lock:
if not self._operations:
return None
root, current = self._operations[0], self._operations[-1]
now = time.monotonic()
return PluginLifecycleActivity(
operation=root.operation, plugin_id=current.plugin_id,
hook=current.hook, elapsed_seconds=now - root.started_at,
hook_elapsed_seconds=now - current.started_at if current.hook else 0,
)