fix(plugin): suppress delayed install reloads (#6484)

This commit is contained in:
InfinityPacer
2026-08-27 23:28:09 +08:00
committed by GitHub
parent 988b425510
commit 513d867dd6
3 changed files with 49 additions and 9 deletions
+19 -3
View File
@@ -3,6 +3,7 @@ import concurrent.futures
import inspect import inspect
import posixpath import posixpath
import threading import threading
import time
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
@@ -167,6 +168,8 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
"""插件管理器""" """插件管理器"""
CONFIG_WATCH = {"DEV", "PLUGIN_AUTO_RELOAD", "PLUGIN_LOCAL_REPO_PATHS"} CONFIG_WATCH = {"DEV", "PLUGIN_AUTO_RELOAD", "PLUGIN_LOCAL_REPO_PATHS"}
AGENT_TOOLS_BUILD_MAX_ATTEMPTS = 3 AGENT_TOOLS_BUILD_MAX_ATTEMPTS = 3
# 略长于 watchfiles 默认 debounce,吸收包写入完成后才交付的延迟批次。
MONITOR_SETTLE_SECONDS = 2.0
def __init__(self): def __init__(self):
"""初始化插件注册表、缓存和开发模式监控状态。""" """初始化插件注册表、缓存和开发模式监控状态。"""
@@ -213,6 +216,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
self._recent_local_sync: Dict[str, float] = {} self._recent_local_sync: Dict[str, float] = {}
self._monitor_suppression_lock = threading.Lock() self._monitor_suppression_lock = threading.Lock()
self._suppressed_monitor_plugins: Dict[str, int] = {} self._suppressed_monitor_plugins: Dict[str, int] = {}
self._monitor_suppressed_until: Dict[str, float] = {}
self._plugin_paths = PluginPathResolver( self._plugin_paths = PluginPathResolver(
runtime_root=get_runtime_setting('ROOT_PATH') / "app" / "plugins", runtime_root=get_runtime_setting('ROOT_PATH') / "app" / "plugins",
running=lambda: self._running_plugins, running=lambda: self._running_plugins,
@@ -724,7 +728,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
@contextmanager @contextmanager
def suppress_plugin_monitor(self, plugin_id: str): def suppress_plugin_monitor(self, plugin_id: str):
"""在插件目录原子更新期间阻止文件监控抢先重载半成品""" """在插件包写入及其文件事件收敛期间阻止监控重复重载"""
with self.mutation("更新插件包"): with self.mutation("更新插件包"):
normalized_id = plugin_id.lower() normalized_id = plugin_id.lower()
with self._monitor_suppression_lock: with self._monitor_suppression_lock:
@@ -738,13 +742,25 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
count = self._suppressed_monitor_plugins.get(normalized_id, 0) count = self._suppressed_monitor_plugins.get(normalized_id, 0)
if count <= 1: if count <= 1:
self._suppressed_monitor_plugins.pop(normalized_id, None) self._suppressed_monitor_plugins.pop(normalized_id, None)
self._monitor_suppressed_until[normalized_id] = (
time.monotonic() + self.MONITOR_SETTLE_SECONDS
)
else: else:
self._suppressed_monitor_plugins[normalized_id] = count - 1 self._suppressed_monitor_plugins[normalized_id] = count - 1
def is_plugin_monitor_suppressed(self, plugin_id: str) -> bool: def is_plugin_monitor_suppressed(self, plugin_id: str) -> bool:
"""判断指定插件是否处于安装或替换写入阶段。""" """判断插件是否处于包写入或延迟文件事件收敛阶段。"""
with self._monitor_suppression_lock: with self._monitor_suppression_lock:
return self._suppressed_monitor_plugins.get(plugin_id.lower(), 0) > 0 normalized_id = plugin_id.lower()
if self._suppressed_monitor_plugins.get(normalized_id, 0) > 0:
return True
suppressed_until = self._monitor_suppressed_until.get(normalized_id)
if suppressed_until is None:
return False
if time.monotonic() < suppressed_until:
return True
self._monitor_suppressed_until.pop(normalized_id, None)
return False
def remove_plugin(self, plugin_id: str): def remove_plugin(self, plugin_id: str):
""" """
-3
View File
@@ -1007,9 +1007,6 @@
"app/schemas/message.py": { "app/schemas/message.py": {
"I001": 1 "I001": 1
}, },
"app/schemas/plugin.py": {
"I001": 1
},
"app/schemas/response.py": { "app/schemas/response.py": {
"I001": 1 "I001": 1
}, },
+30 -3
View File
@@ -907,8 +907,11 @@ def test_monitor_reload_refreshes_source_and_instance_routes(monkeypatch) -> Non
] ]
def test_plugin_monitor_suppression_is_reference_counted(monkeypatch) -> None: def test_plugin_monitor_suppression_covers_delayed_write_events(
"""同一插件的重叠写入必须等最后一个事务退出后才解除监控抑制。""" monkeypatch,
tmp_path,
) -> None:
"""重叠写入退出后的延迟批次不重复重载,后续真实修改仍可重载。"""
_reset_plugin_manager() _reset_plugin_manager()
reset_plugin_system() reset_plugin_system()
_patch_runtime_settings( _patch_runtime_settings(
@@ -917,7 +920,26 @@ def test_plugin_monitor_suppression_is_reference_counted(monkeypatch) -> None:
PLUGIN_AUTO_RELOAD=False, PLUGIN_AUTO_RELOAD=False,
ROOT_PATH=MagicMock(), ROOT_PATH=MagicMock(),
) )
now = [100.0]
monkeypatch.setattr(plugin_manager_module.time, "monotonic", lambda: now[0])
manager = PluginManager() manager = PluginManager()
reload_plugin = MagicMock()
monitor = PluginChangeMonitor(
runtime_root=tmp_path,
local_roots=lambda: [],
stop_event=threading.Event(),
recent_sync={},
federated_change=lambda _path: None,
runtime_plugin=lambda _path: "DemoPlugin",
local_candidate=lambda _path: None,
sync_local=MagicMock(),
reload_plugin=reload_plugin,
dependency_manifest_status=lambda _path: None,
watch=lambda *_args, **_kwargs: (),
log=MagicMock(),
monitor_suppressed=manager.is_plugin_monitor_suppressed,
)
changes = {("modified", str(tmp_path / "demo" / "plugin.py"))}
with manager.suppress_plugin_monitor("DemoPlugin"): with manager.suppress_plugin_monitor("DemoPlugin"):
assert manager.is_plugin_monitor_suppressed("demoplugin") is True assert manager.is_plugin_monitor_suppressed("demoplugin") is True
@@ -925,7 +947,12 @@ def test_plugin_monitor_suppression_is_reference_counted(monkeypatch) -> None:
assert manager.is_plugin_monitor_suppressed("DemoPlugin") is True assert manager.is_plugin_monitor_suppressed("DemoPlugin") is True
assert manager.is_plugin_monitor_suppressed("DemoPlugin") is True assert manager.is_plugin_monitor_suppressed("DemoPlugin") is True
assert manager.is_plugin_monitor_suppressed("DemoPlugin") is False monitor._process_changes(changes)
reload_plugin.assert_not_called()
now[0] += manager.MONITOR_SETTLE_SECONDS
monitor._process_changes(changes)
reload_plugin.assert_called_once_with("DemoPlugin")
_reset_plugin_manager() _reset_plugin_manager()