mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 12:06:51 +08:00
fix(runtime): 收敛模块关闭线程所有权 (#6443)
* fix(runtime): bound module shutdown owners * fix(runtime): declare blocking shutdown owners --------- Co-authored-by: jxxghp <jxxghp@gmail.com>
This commit is contained in:
@@ -224,6 +224,22 @@ preflight 会验证 Python 3.14、GIL 状态、`thread_inherit_context`、MovieP
|
||||
依赖、语义、驱动、启动或样本完整性不成立。该工具只用于隔离的本地长 A/B,不接真实凭据、用户数据库、
|
||||
媒体目录或外网,也不加入常规 CI。
|
||||
|
||||
## 模块关闭事件循环 A/B
|
||||
|
||||
`module_shutdown_ab.py` 使用隔离资源 owner,分别测量 `stop_modules()` 内部同步关闭和生命周期总入口的
|
||||
同步关闭。默认制造 50ms 同步等待,并观察 10ms 心跳是否在关闭完成前执行:
|
||||
|
||||
```bash
|
||||
../.venv-test/bin/python scripts/perf/module_shutdown_ab.py \
|
||||
--block-ms 50 \
|
||||
--heartbeat-ms 10 \
|
||||
--samples 7
|
||||
```
|
||||
|
||||
分别在 Before/After revision 运行相同参数并保存 JSON。总关闭耗时应保持接近固定等待时间;心跳中位延迟
|
||||
用于判断事件循环是否被同步 owner 占用,所有样本在关闭完成前执行心跳属于正确性门禁。探针不启动真实
|
||||
模块、线程池、数据库、配置或网络。
|
||||
|
||||
## TaskRegistry 跨线程提交 A/B
|
||||
|
||||
`task_registry_ab.py` 验证目标事件循环尚未分发 callback 时执行 shutdown,pending completion 与原始
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
"""测量同步资源关闭期间的事件循环响应与关闭总耗时。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.testing.bootstrap import install_sites_stub, isolate_config_dir
|
||||
|
||||
isolate_config_dir()
|
||||
install_sites_stub()
|
||||
|
||||
from app.startup import lifecycle
|
||||
from app.startup.initializers import modules as modules_initializer
|
||||
|
||||
|
||||
async def _noop_async() -> None:
|
||||
"""提供不产生外部副作用的异步关闭替身。"""
|
||||
|
||||
|
||||
def _noop_sync() -> None:
|
||||
"""提供不产生外部副作用的同步关闭替身。"""
|
||||
|
||||
|
||||
def configure_module_probe(block_seconds: float) -> None:
|
||||
"""把模块关闭依赖替换为隔离 owner,仅保留一个固定同步等待。"""
|
||||
|
||||
def blocking_module_shutdown() -> bool:
|
||||
time.sleep(block_seconds)
|
||||
return True
|
||||
|
||||
modules_initializer.ModuleManager = lambda: SimpleNamespace(
|
||||
shutdown=blocking_module_shutdown
|
||||
)
|
||||
modules_initializer.EventManager = lambda: SimpleNamespace(
|
||||
stop_async=_noop_async
|
||||
)
|
||||
modules_initializer.DohHelper = lambda: SimpleNamespace(shutdown=_noop_sync)
|
||||
modules_initializer.ThreadHelper = lambda: SimpleNamespace(shutdown=_noop_sync)
|
||||
modules_initializer.RedisHelper = lambda: SimpleNamespace(close=_noop_sync)
|
||||
modules_initializer.AsyncRedisHelper = lambda: SimpleNamespace(close=_noop_async)
|
||||
modules_initializer.close_image_proxy_block_log_coalescer = _noop_async
|
||||
modules_initializer.close_browser_sessions = _noop_sync
|
||||
modules_initializer.stop_managed_resources = _noop_async
|
||||
modules_initializer.stop_message = _noop_sync
|
||||
modules_initializer.shutdown_web_agent_background_tasks = _noop_async
|
||||
modules_initializer.wait_web_agent_background_tasks = _noop_async
|
||||
modules_initializer.get_configured_agent_chat_persistence = lambda: SimpleNamespace(
|
||||
begin_shutdown=_noop_sync,
|
||||
shutdown=_noop_async,
|
||||
)
|
||||
|
||||
async def stop_database_worker() -> None:
|
||||
modules_initializer._database_worker = None
|
||||
|
||||
modules_initializer.stop_database_worker = stop_database_worker
|
||||
modules_initializer.close_database = _noop_async
|
||||
modules_initializer.stop_frontend = _noop_sync
|
||||
modules_initializer.clear_temp = _noop_sync
|
||||
modules_initializer._database_worker = None
|
||||
|
||||
|
||||
async def sample(
|
||||
shutdown: Callable[[], Awaitable[object]],
|
||||
*,
|
||||
heartbeat_seconds: float,
|
||||
) -> dict[str, float | bool]:
|
||||
"""执行一次关闭并测量同期心跳的实际唤醒延迟。"""
|
||||
loop = asyncio.get_running_loop()
|
||||
started_at = loop.time()
|
||||
heartbeat_at: float | None = None
|
||||
|
||||
async def heartbeat() -> None:
|
||||
nonlocal heartbeat_at
|
||||
await asyncio.sleep(heartbeat_seconds)
|
||||
heartbeat_at = loop.time()
|
||||
|
||||
heartbeat_task = asyncio.create_task(heartbeat())
|
||||
await shutdown()
|
||||
shutdown_finished_at = loop.time()
|
||||
completed_before_shutdown = heartbeat_task.done()
|
||||
await heartbeat_task
|
||||
assert heartbeat_at is not None
|
||||
return {
|
||||
"heartbeat_delay_ms": (heartbeat_at - started_at) * 1000,
|
||||
"heartbeat_completed_before_shutdown": completed_before_shutdown,
|
||||
"shutdown_ms": (shutdown_finished_at - started_at) * 1000,
|
||||
}
|
||||
|
||||
|
||||
async def run_samples(
|
||||
*,
|
||||
block_seconds: float,
|
||||
heartbeat_seconds: float,
|
||||
samples: int,
|
||||
) -> dict[str, object]:
|
||||
"""顺序采集模块内部和生命周期总入口两类同步关闭样本。"""
|
||||
configure_module_probe(block_seconds)
|
||||
module_samples = [
|
||||
await sample(
|
||||
modules_initializer.stop_modules,
|
||||
heartbeat_seconds=heartbeat_seconds,
|
||||
)
|
||||
for _ in range(samples)
|
||||
]
|
||||
lifecycle_samples = [
|
||||
await sample(
|
||||
lambda: lifecycle.run_shutdown_step(
|
||||
"probe.sync_owner",
|
||||
lifecycle.offload_shutdown_callback(
|
||||
lambda: time.sleep(block_seconds)
|
||||
),
|
||||
timeout_seconds=max(1.0, block_seconds * 4),
|
||||
),
|
||||
heartbeat_seconds=heartbeat_seconds,
|
||||
)
|
||||
for _ in range(samples)
|
||||
]
|
||||
|
||||
def summarize(values: list[dict[str, float | bool]]) -> dict[str, object]:
|
||||
return {
|
||||
"samples": values,
|
||||
"heartbeat_delay_median_ms": statistics.median(
|
||||
float(value["heartbeat_delay_ms"]) for value in values
|
||||
),
|
||||
"shutdown_median_ms": statistics.median(
|
||||
float(value["shutdown_ms"]) for value in values
|
||||
),
|
||||
"heartbeat_completed_before_shutdown": all(
|
||||
bool(value["heartbeat_completed_before_shutdown"])
|
||||
for value in values
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
"block_ms": block_seconds * 1000,
|
||||
"heartbeat_target_ms": heartbeat_seconds * 1000,
|
||||
"module_step": summarize(module_samples),
|
||||
"lifecycle_step": summarize(lifecycle_samples),
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""解析固定阻塞、心跳间隔和样本数。"""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--block-ms", type=float, default=50.0)
|
||||
parser.add_argument("--heartbeat-ms", type=float, default=10.0)
|
||||
parser.add_argument("--samples", type=int, default=7)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""运行隔离样本并输出 JSON。"""
|
||||
args = parse_args()
|
||||
if args.block_ms <= 0 or args.heartbeat_ms <= 0 or args.samples < 1:
|
||||
raise SystemExit("block-ms、heartbeat-ms 和 samples 必须大于 0")
|
||||
result = asyncio.run(
|
||||
run_samples(
|
||||
block_seconds=args.block_ms / 1000,
|
||||
heartbeat_seconds=args.heartbeat_ms / 1000,
|
||||
samples=args.samples,
|
||||
)
|
||||
)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user