mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
feat: observe legacy module contract hits
This commit is contained in:
@@ -13,6 +13,7 @@ from app.runtime.observability import observe_duration, record_metric
|
||||
from app.runtime.extensions.module.contracts import (
|
||||
diagnose_module_callable,
|
||||
get_module_method_contract,
|
||||
is_explicit_module_method,
|
||||
)
|
||||
from app.schemas.exception import RateLimitExceededException
|
||||
|
||||
@@ -124,6 +125,11 @@ class ModuleInvocationDispatcher:
|
||||
func = module_dict.get(method)
|
||||
if not func:
|
||||
continue
|
||||
self._record_legacy_hit(
|
||||
method,
|
||||
caller_type="plugin",
|
||||
abi_source="third_party_plugin",
|
||||
)
|
||||
self._diagnose_callable(method, func, f"插件 {plugin_id}")
|
||||
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
|
||||
if self.is_valid_empty(result):
|
||||
@@ -172,6 +178,11 @@ class ModuleInvocationDispatcher:
|
||||
func = module_dict.get(method)
|
||||
if not func:
|
||||
continue
|
||||
self._record_legacy_hit(
|
||||
method,
|
||||
caller_type="plugin",
|
||||
abi_source="third_party_plugin",
|
||||
)
|
||||
self._diagnose_callable(method, func, f"插件 {plugin_id}")
|
||||
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
|
||||
if self.is_valid_empty(result):
|
||||
@@ -219,6 +230,11 @@ class ModuleInvocationDispatcher:
|
||||
module_name = self._module_name(module, module_id)
|
||||
try:
|
||||
func = getattr(module, method)
|
||||
self._record_legacy_hit(
|
||||
method,
|
||||
caller_type="system",
|
||||
abi_source="host_module",
|
||||
)
|
||||
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
|
||||
if self.is_valid_empty(result):
|
||||
result = func(*args, **kwargs)
|
||||
@@ -267,6 +283,11 @@ class ModuleInvocationDispatcher:
|
||||
module_name = self._module_name(module, module_id)
|
||||
try:
|
||||
func = getattr(module, method)
|
||||
self._record_legacy_hit(
|
||||
method,
|
||||
caller_type="system",
|
||||
abi_source="host_module",
|
||||
)
|
||||
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
|
||||
if self.is_valid_empty(result):
|
||||
result = await self._async_call(func, *args, **kwargs)
|
||||
@@ -307,6 +328,22 @@ class ModuleInvocationDispatcher:
|
||||
provider_type=provider_type,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _record_legacy_hit(
|
||||
method: str,
|
||||
*,
|
||||
caller_type: str,
|
||||
abi_source: str,
|
||||
) -> None:
|
||||
"""记录未知动态方法的兼容命中,便于按真实调用逐项迁移。"""
|
||||
if not is_explicit_module_method(method):
|
||||
record_metric(
|
||||
"module.contract.legacy_hit",
|
||||
method=method,
|
||||
caller_type=caller_type,
|
||||
abi_source=abi_source,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _diagnose_callable(
|
||||
method: str,
|
||||
|
||||
@@ -37,6 +37,11 @@ METRIC_SPECS = {
|
||||
MetricSpec("event.handler.duration", MetricKind.HISTOGRAM, frozenset({"event_type", "handler_type", "outcome"})),
|
||||
MetricSpec("module.provider.duration", MetricKind.HISTOGRAM, frozenset({"method", "provider_type", "outcome"})),
|
||||
MetricSpec("module.provider.timeout", MetricKind.COUNTER, frozenset({"method", "provider_type"})),
|
||||
MetricSpec(
|
||||
"module.contract.legacy_hit",
|
||||
MetricKind.COUNTER,
|
||||
frozenset({"method", "caller_type", "abi_source"}),
|
||||
),
|
||||
MetricSpec("scheduler.job.duration", MetricKind.HISTOGRAM, frozenset({"owner", "outcome"})),
|
||||
MetricSpec("scheduler.job.overlap_skip", MetricKind.COUNTER, frozenset({"owner"})),
|
||||
MetricSpec("scheduler.job.retry", MetricKind.COUNTER, frozenset({"owner"})),
|
||||
|
||||
@@ -644,6 +644,8 @@ ModuleMethodSpec(
|
||||
- 契约清单现覆盖静态扫描到的 211 个宿主字符串调用,并保留一个暂未被宿主调用的 `send_message` 公开能力,
|
||||
共 212 个显式 V2 spec。原先仅按 prefix 分类或落入默认 legacy 的宿主方法均获得稳定 family、输入合同、
|
||||
结果合同、执行、超时和错误语义;未知第三方自定义方法仍走开放 legacy fallback,不拒绝加载或执行。
|
||||
- 未知动态方法在真实 provider 命中时记录 `module.contract.legacy_hit`,区分插件/宿主调用方和 ABI 来源;
|
||||
该指标只在 callable 实际存在并准备执行时递增,不改变未知第三方方法的开放 fallback、聚合或异常语义。
|
||||
|
||||
#### ARCH-241:Event Contract Registry
|
||||
|
||||
|
||||
@@ -222,6 +222,71 @@ def test_plugin_non_mapping_module_decl_is_reported_and_skipped() -> None:
|
||||
plugin_error.assert_called_once()
|
||||
|
||||
|
||||
def test_unknown_plugin_method_records_legacy_abi_hit(monkeypatch) -> None:
|
||||
"""未知第三方方法继续执行,同时记录可迁移的 legacy ABI 来源。"""
|
||||
hits = []
|
||||
monkeypatch.setattr(
|
||||
"app.runtime.extensions.module.dispatcher.record_metric",
|
||||
lambda name, **labels: hits.append((name, labels)),
|
||||
)
|
||||
dispatcher, _, _, _ = _dispatcher(
|
||||
plugins={("P1", "插件一"): {"third_party_custom": lambda: "ok"}},
|
||||
)
|
||||
|
||||
assert dispatcher.dispatch("third_party_custom") == "ok"
|
||||
assert hits == [
|
||||
(
|
||||
"module.contract.legacy_hit",
|
||||
{
|
||||
"method": "third_party_custom",
|
||||
"caller_type": "plugin",
|
||||
"abi_source": "third_party_plugin",
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_unknown_host_method_records_legacy_abi_hit(monkeypatch) -> None:
|
||||
"""宿主临时新增而未登记的方法保持执行并留下迁移信号。"""
|
||||
hits = []
|
||||
monkeypatch.setattr(
|
||||
"app.runtime.extensions.module.dispatcher.record_metric",
|
||||
lambda name, **labels: hits.append((name, labels)),
|
||||
)
|
||||
|
||||
class LegacyModule:
|
||||
"""提供未进入清单的宿主兼容方法。"""
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
"""返回测试模块名称。"""
|
||||
return "旧模块"
|
||||
|
||||
@staticmethod
|
||||
def get_priority() -> int:
|
||||
"""返回稳定测试优先级。"""
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def third_party_host() -> str:
|
||||
"""返回兼容方法结果。"""
|
||||
return "ok"
|
||||
|
||||
dispatcher, _, _, _ = _dispatcher(modules=[LegacyModule()])
|
||||
|
||||
assert dispatcher.dispatch("third_party_host") == "ok"
|
||||
assert hits == [
|
||||
(
|
||||
"module.contract.legacy_hit",
|
||||
{
|
||||
"method": "third_party_host",
|
||||
"caller_type": "system",
|
||||
"abi_source": "host_module",
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_plugin_non_mapping_module_decl_is_reported_and_skipped() -> None:
|
||||
"""异步路径下坏插件同样被隔离,嵌套补丁场景不再冒泡击穿调度。"""
|
||||
|
||||
Reference in New Issue
Block a user