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:
InfinityPacer
2026-08-25 06:58:31 +08:00
committed by GitHub
co-authored by jxxghp
parent 6c334f7b9b
commit be7dfd77a3
8 changed files with 421 additions and 48 deletions
+16
View File
@@ -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 时执行 shutdownpending completion 与原始
+179
View File
@@ -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()