mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 15:09:46 +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:
@@ -84,7 +84,7 @@ class Telegram:
|
||||
_typing_command_max_duration_seconds = 30
|
||||
_typing_callback_max_duration_seconds = 60
|
||||
_typing_join_timeout_seconds = 1
|
||||
_polling_join_timeout_seconds = 10
|
||||
_shutdown_timeout_seconds = 10
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1743,9 +1743,25 @@ class Telegram:
|
||||
# 清理菜单命令
|
||||
self._bot.delete_my_commands()
|
||||
|
||||
@staticmethod
|
||||
def _stop_bot_with_deadline(bot: TeleBot, deadline: float) -> bool:
|
||||
"""停止 SDK polling,并在共享 deadline 内等待 worker 收敛。"""
|
||||
bot.stop_polling()
|
||||
if not bot.threaded or not bot.worker_pool:
|
||||
return True
|
||||
|
||||
workers = tuple(bot.worker_pool.workers)
|
||||
for worker in workers:
|
||||
worker.stop()
|
||||
for worker in workers:
|
||||
if worker is threading.current_thread():
|
||||
continue
|
||||
worker.join(timeout=max(0.0, deadline - time.monotonic()))
|
||||
return all(not worker.is_alive() for worker in workers)
|
||||
|
||||
def stop(self) -> bool:
|
||||
"""
|
||||
停止 Telegram 消息接收服务,并返回 polling/typing owner 是否收敛。
|
||||
停止 Telegram 消息接收服务,并返回 SDK/polling/typing owner 是否收敛。
|
||||
"""
|
||||
converged = True
|
||||
with self._typing_lifecycle_lock:
|
||||
@@ -1757,16 +1773,24 @@ class Telegram:
|
||||
|
||||
bot = self._bot
|
||||
polling_thread = self._polling_thread
|
||||
deadline = time.monotonic() + self._shutdown_timeout_seconds
|
||||
transport_converged = True
|
||||
if bot:
|
||||
bot.stop_bot()
|
||||
if not self._stop_bot_with_deadline(bot, deadline):
|
||||
converged = False
|
||||
transport_converged = False
|
||||
logger.error("Telegram SDK worker 未在关闭预算内退出")
|
||||
if (
|
||||
polling_thread
|
||||
and polling_thread.is_alive()
|
||||
and polling_thread is not threading.current_thread()
|
||||
):
|
||||
polling_thread.join(timeout=self._polling_join_timeout_seconds)
|
||||
polling_thread.join(timeout=max(0.0, deadline - time.monotonic()))
|
||||
if polling_thread and polling_thread.is_alive():
|
||||
logger.error("Telegram polling 线程未在关闭预算内退出")
|
||||
converged = False
|
||||
transport_converged = False
|
||||
if not transport_converged:
|
||||
return False
|
||||
self._polling_thread = None
|
||||
self._bot = None
|
||||
|
||||
@@ -27,6 +27,7 @@ from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.events import EventHandlerBinding, EventManager
|
||||
from app.runtime.execution import run_in_threadpool_to_completion
|
||||
from app.runtime.observability import record_metric
|
||||
from app.runtime.state import SystemHelper
|
||||
from app.runtime.settings import configure_runtime_setting_provider
|
||||
@@ -611,12 +612,16 @@ async def stop_modules() -> bool:
|
||||
name: str,
|
||||
callback: Callable[[], object],
|
||||
*,
|
||||
offload: bool = False,
|
||||
record_failure: bool = True,
|
||||
) -> bool:
|
||||
"""执行单个关闭步骤,失败时继续收口并保留诚实结果。"""
|
||||
nonlocal all_converged
|
||||
try:
|
||||
result = callback()
|
||||
if offload:
|
||||
result = await run_in_threadpool_to_completion(callback)
|
||||
else:
|
||||
result = callback()
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
converged = result is not False
|
||||
@@ -633,14 +638,14 @@ async def stop_modules() -> bool:
|
||||
return converged
|
||||
|
||||
await run_step("图片代理安全日志合并器", close_image_proxy_block_log_coalescer)
|
||||
await run_step("模块", lambda: ModuleManager().shutdown())
|
||||
await run_step("模块", lambda: ModuleManager().shutdown(), offload=True)
|
||||
await run_step("事件消费", lambda: EventManager().stop_async())
|
||||
await run_step("浏览器会话", close_browser_sessions)
|
||||
await run_step("浏览器会话", close_browser_sessions, offload=True)
|
||||
await run_step("托管资源", stop_managed_resources)
|
||||
await run_step("DoH服务", lambda: DohHelper().shutdown())
|
||||
await run_step("线程池", lambda: ThreadHelper().shutdown())
|
||||
await run_step("消息服务", stop_message)
|
||||
await run_step("Redis缓存连接", lambda: RedisHelper().close())
|
||||
await run_step("DoH服务", lambda: DohHelper().shutdown(), offload=True)
|
||||
await run_step("线程池", lambda: ThreadHelper().shutdown(), offload=True)
|
||||
await run_step("消息服务", stop_message, offload=True)
|
||||
await run_step("Redis缓存连接", lambda: RedisHelper().close(), offload=True)
|
||||
await run_step("异步Redis缓存连接", lambda: AsyncRedisHelper().close())
|
||||
# Web Agent 的取消 finally 可能还要写入最终展示快照,必须先完成任务收尾,再关闭写入准入。
|
||||
web_agent_drained = await run_step(
|
||||
@@ -672,8 +677,8 @@ async def stop_modules() -> bool:
|
||||
else:
|
||||
all_converged = False
|
||||
logger.error("数据库任务未收敛,跳过数据库连接关闭以避免运行中事务使用已释放连接")
|
||||
await run_step("前端服务", stop_frontend)
|
||||
await run_step("临时文件", clear_temp)
|
||||
await run_step("前端服务", stop_frontend, offload=True)
|
||||
await run_step("临时文件", clear_temp, offload=True)
|
||||
return all_converged
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import inspect
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Callable
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
@@ -33,6 +33,7 @@ from app.foundation.environment import is_free_threaded_runtime, is_gil_enabled
|
||||
|
||||
settings = RuntimeSettingsCompat()
|
||||
from app.runtime.health import get_application_health
|
||||
from app.runtime.execution import run_in_threadpool_to_completion
|
||||
from app.runtime.topology import validate_process_topology
|
||||
from app.runtime.tasks import TaskRegistry, configure_task_registry
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
@@ -135,32 +136,38 @@ async def run_shutdown_step(
|
||||
timeout_seconds: float | None = None,
|
||||
) -> bool:
|
||||
"""在有限预算内执行关闭阶段,并返回资源 owner 是否已经收敛。"""
|
||||
try:
|
||||
|
||||
async def invoke() -> object:
|
||||
"""在主循环调用 owner,并等待其可能返回的异步结果。"""
|
||||
result = callback()
|
||||
if inspect.isawaitable(result):
|
||||
task = asyncio.ensure_future(result)
|
||||
return await result
|
||||
return result
|
||||
|
||||
def _consume_shutdown_result(done: asyncio.Future) -> None:
|
||||
"""消费延迟收敛任务的最终异常,避免事件循环产生未取回异常。"""
|
||||
try:
|
||||
done.result()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as err:
|
||||
logger.error(f"关闭{name}最终收尾失败:{err}")
|
||||
try:
|
||||
task = asyncio.create_task(invoke(), name=f"shutdown.{name}")
|
||||
|
||||
task.add_done_callback(_consume_shutdown_result)
|
||||
if timeout_seconds:
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
asyncio.shield(task), timeout=timeout_seconds
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("关闭%s超时,已请求取消并保留未收敛任务", name)
|
||||
task.cancel()
|
||||
return False
|
||||
else:
|
||||
result = await task
|
||||
def _consume_shutdown_result(done: asyncio.Future) -> None:
|
||||
"""消费延迟收敛任务的最终异常,避免事件循环产生未取回异常。"""
|
||||
try:
|
||||
done.result()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as err:
|
||||
logger.error(f"关闭{name}最终收尾失败:{err}")
|
||||
|
||||
task.add_done_callback(_consume_shutdown_result)
|
||||
if timeout_seconds:
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
asyncio.shield(task), timeout=timeout_seconds
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("关闭%s超时,已请求取消并保留未收敛任务", name)
|
||||
task.cancel()
|
||||
return False
|
||||
else:
|
||||
result = await task
|
||||
if result is False:
|
||||
logger.error("关闭%s未收敛,资源所有权保持不变", name)
|
||||
return False
|
||||
@@ -170,6 +177,17 @@ async def run_shutdown_step(
|
||||
return False
|
||||
|
||||
|
||||
def offload_shutdown_callback(
|
||||
callback: Callable[[], object],
|
||||
) -> Callable[[], Awaitable[object]]:
|
||||
"""把明确会阻塞的同步关闭 owner 包装为异步生命周期回调。"""
|
||||
|
||||
async def invoke() -> object:
|
||||
return await run_in_threadpool_to_completion(callback)
|
||||
|
||||
return invoke
|
||||
|
||||
|
||||
async def run_startup_step(
|
||||
name: str,
|
||||
callback: Callable[[], object],
|
||||
@@ -384,7 +402,7 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]:
|
||||
dependencies=("插件备份恢复",),
|
||||
mode=LifecycleMode.NORMAL_ONLY,
|
||||
start=init_plugins,
|
||||
stop=finalize_plugins,
|
||||
stop=offload_shutdown_callback(finalize_plugins),
|
||||
start_order=90,
|
||||
stop_order=60,
|
||||
start_timeout_seconds=300,
|
||||
@@ -395,7 +413,7 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]:
|
||||
name="插件变更监控",
|
||||
dependencies=("插件",),
|
||||
mode=LifecycleMode.NORMAL_ONLY,
|
||||
stop=stop_plugin_monitor,
|
||||
stop=offload_shutdown_callback(stop_plugin_monitor),
|
||||
stop_order=8,
|
||||
stop_timeout_seconds=10,
|
||||
stop_failure=LifecycleFailurePolicy.FAIL_FAST,
|
||||
@@ -405,7 +423,7 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]:
|
||||
dependencies=("插件",),
|
||||
mode=LifecycleMode.NORMAL_ONLY,
|
||||
start=init_scheduler,
|
||||
stop=stop_scheduler,
|
||||
stop=offload_shutdown_callback(stop_scheduler),
|
||||
start_order=100,
|
||||
stop_order=50,
|
||||
start_timeout_seconds=120,
|
||||
@@ -496,7 +514,7 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]:
|
||||
dependencies=("命令服务",),
|
||||
mode=LifecycleMode.NORMAL_ONLY,
|
||||
start=init_workflow,
|
||||
stop=stop_workflow,
|
||||
stop=offload_shutdown_callback(stop_workflow),
|
||||
start_order=140,
|
||||
stop_order=20,
|
||||
start_timeout_seconds=120,
|
||||
@@ -507,7 +525,9 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]:
|
||||
name="插件备份",
|
||||
dependencies=("插件",),
|
||||
mode=LifecycleMode.NORMAL_ONLY,
|
||||
stop=lambda: SystemChain().backup_plugins(),
|
||||
stop=offload_shutdown_callback(
|
||||
lambda: SystemChain().backup_plugins()
|
||||
),
|
||||
stop_order=10,
|
||||
stop_timeout_seconds=300,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user