mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
refactor: unify capability shutdown convergence
This commit is contained in:
@@ -20,10 +20,7 @@ from app.agent.capabilities.adapter import (
|
||||
build_agent_capability_registry,
|
||||
should_run_agent_service,
|
||||
)
|
||||
from app.runtime.capabilities.model import (
|
||||
CapabilityLifecycleState,
|
||||
CapabilityMaterializationState,
|
||||
)
|
||||
from app.runtime.capabilities.model import CapabilityMaterializationState
|
||||
from app.runtime.capabilities.runtime import CapabilityRuntime
|
||||
|
||||
|
||||
@@ -146,10 +143,8 @@ async def close_materialized_terminal_sessions() -> None:
|
||||
|
||||
|
||||
async def begin_agent_shutdown() -> bool:
|
||||
"""不可逆关闭首用闸门,并返回 Agent service 是否真实收敛。"""
|
||||
"""不可逆关闭首用闸门,并返回全部 Agent 能力是否真实收敛。"""
|
||||
runtime = _ensure_runtime()
|
||||
await runtime.shutdown_async(reason="application_shutdown")
|
||||
return (
|
||||
runtime.snapshot(AGENT_SERVICE_CAPABILITY_ID).lifecycle
|
||||
is CapabilityLifecycleState.STOPPED
|
||||
return await runtime.shutdown_async(
|
||||
reason="application_shutdown",
|
||||
)
|
||||
|
||||
@@ -72,13 +72,17 @@ class TelegramModule(_MessageChannelModuleBase[Telegram]):
|
||||
"""
|
||||
return 0
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止模块"""
|
||||
def stop(self) -> bool:
|
||||
"""停止全部 Telegram 实例,并返回资源是否全部收敛。"""
|
||||
converged = True
|
||||
for client in self.get_instances().values():
|
||||
try:
|
||||
client.stop()
|
||||
if client.stop() is False:
|
||||
converged = False
|
||||
except Exception as err:
|
||||
logger.error(f"停止Telegram模块实例失败:{err}")
|
||||
converged = False
|
||||
return converged
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
"""
|
||||
|
||||
@@ -84,6 +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
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1742,21 +1743,32 @@ class Telegram:
|
||||
# 清理菜单命令
|
||||
self._bot.delete_my_commands()
|
||||
|
||||
def stop(self) -> None:
|
||||
def stop(self) -> bool:
|
||||
"""
|
||||
停止Telegram消息接收服务
|
||||
停止 Telegram 消息接收服务,并返回 polling/typing owner 是否收敛。
|
||||
"""
|
||||
converged = True
|
||||
with self._typing_lifecycle_lock:
|
||||
self._typing_accepting = False
|
||||
# 封口与 owner 快照处于同一临界区,停止后不会漏掉并发新增任务。
|
||||
for chat_id in list(self._typing_tasks.keys()):
|
||||
self._stop_typing_task(chat_id)
|
||||
if not self._bot:
|
||||
return
|
||||
if not self._stop_typing_task(chat_id):
|
||||
converged = False
|
||||
|
||||
self._bot.stop_bot()
|
||||
if self._polling_thread:
|
||||
self._polling_thread.join()
|
||||
bot = self._bot
|
||||
polling_thread = self._polling_thread
|
||||
if bot:
|
||||
bot.stop_bot()
|
||||
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)
|
||||
if polling_thread and polling_thread.is_alive():
|
||||
logger.error("Telegram polling 线程未在关闭预算内退出")
|
||||
return False
|
||||
self._polling_thread = None
|
||||
self._bot = None
|
||||
logger.info("Telegram消息接收服务已停止")
|
||||
return converged
|
||||
|
||||
@@ -1126,7 +1126,8 @@ class CapabilityRuntime:
|
||||
"""同步撤销并停止能力资源;物化实现保留供后续显式重启。"""
|
||||
self._stop_sync(capability_id, reason=reason, shutdown=False)
|
||||
|
||||
def _stop_sync(self, capability_id: str, *, reason: str, shutdown: bool) -> None:
|
||||
def _stop_sync(self, capability_id: str, *, reason: str, shutdown: bool) -> bool:
|
||||
"""停止同步能力并返回资源 owner 是否已经真实收敛。"""
|
||||
state = self._state(capability_id)
|
||||
adapter = self._adapter(state, AdapterExecutionMode.SYNC)
|
||||
while True:
|
||||
@@ -1146,7 +1147,7 @@ class CapabilityRuntime:
|
||||
if stop_owner is None and pending is None:
|
||||
if state.lifecycle is not CapabilityLifecycleState.FAILED:
|
||||
state.lifecycle = CapabilityLifecycleState.STOPPED
|
||||
return
|
||||
return True
|
||||
state.generation += 1
|
||||
generation = state.generation
|
||||
future: Future[Any] = Future()
|
||||
@@ -1166,8 +1167,10 @@ class CapabilityRuntime:
|
||||
except BaseException:
|
||||
if not shutdown:
|
||||
raise
|
||||
if waiter_operation == "stop":
|
||||
return False
|
||||
if waiter_operation == "stop":
|
||||
return
|
||||
return True
|
||||
continue
|
||||
break
|
||||
|
||||
@@ -1207,6 +1210,7 @@ class CapabilityRuntime:
|
||||
reason=reason,
|
||||
started_at=started_at,
|
||||
)
|
||||
return True
|
||||
except BaseException as error:
|
||||
with state.lock:
|
||||
state.lifecycle = CapabilityLifecycleState.FAILED
|
||||
@@ -1229,12 +1233,20 @@ class CapabilityRuntime:
|
||||
)
|
||||
if not shutdown:
|
||||
raise operation_error from error
|
||||
return False
|
||||
|
||||
async def stop_async(self, capability_id: str, *, reason: str) -> None:
|
||||
"""异步撤销并停止能力资源。"""
|
||||
await self._stop_async(capability_id, reason=reason, shutdown=False)
|
||||
|
||||
async def _stop_async(self, capability_id: str, *, reason: str, shutdown: bool) -> None:
|
||||
async def _stop_async(
|
||||
self,
|
||||
capability_id: str,
|
||||
*,
|
||||
reason: str,
|
||||
shutdown: bool,
|
||||
) -> bool:
|
||||
"""停止异步能力并返回资源 owner 是否已经真实收敛。"""
|
||||
state = self._state(capability_id)
|
||||
adapter = self._adapter(state, AdapterExecutionMode.ASYNC)
|
||||
while True:
|
||||
@@ -1254,7 +1266,7 @@ class CapabilityRuntime:
|
||||
if stop_owner is None and pending is None:
|
||||
if state.lifecycle is not CapabilityLifecycleState.FAILED:
|
||||
state.lifecycle = CapabilityLifecycleState.STOPPED
|
||||
return
|
||||
return True
|
||||
state.generation += 1
|
||||
generation = state.generation
|
||||
future: Future[Any] = Future()
|
||||
@@ -1274,8 +1286,10 @@ class CapabilityRuntime:
|
||||
except BaseException:
|
||||
if not shutdown:
|
||||
raise
|
||||
if waiter_operation == "stop":
|
||||
return False
|
||||
if waiter_operation == "stop":
|
||||
return
|
||||
return True
|
||||
continue
|
||||
break
|
||||
|
||||
@@ -1321,6 +1335,7 @@ class CapabilityRuntime:
|
||||
reason=reason,
|
||||
started_at=started_at,
|
||||
)
|
||||
return True
|
||||
except BaseException as error:
|
||||
with state.lock:
|
||||
state.lifecycle = CapabilityLifecycleState.FAILED
|
||||
@@ -1343,9 +1358,10 @@ class CapabilityRuntime:
|
||||
)
|
||||
if not shutdown:
|
||||
raise operation_error from error
|
||||
return False
|
||||
|
||||
def shutdown(self, *, reason: str) -> None:
|
||||
"""不可逆关闭仅含同步 adapter 的 Runtime,并阻止并发首启重新发布。"""
|
||||
def shutdown(self, *, reason: str) -> bool:
|
||||
"""不可逆关闭同步 Runtime,并返回全部 owner 是否收敛。"""
|
||||
async_kinds = {
|
||||
kind
|
||||
for kind, adapter in self._adapters.items()
|
||||
@@ -1357,21 +1373,32 @@ class CapabilityRuntime:
|
||||
)
|
||||
with self._shutdown_lock:
|
||||
self._shutdown = True
|
||||
converged = True
|
||||
for spec in self._registry.list_specs():
|
||||
self._stop_sync(spec.id, reason=reason, shutdown=True)
|
||||
if not self._stop_sync(spec.id, reason=reason, shutdown=True):
|
||||
converged = False
|
||||
return converged
|
||||
|
||||
async def shutdown_async(self, *, reason: str) -> None:
|
||||
"""不可逆关闭混合同步/异步 adapter 的 Runtime。"""
|
||||
async def shutdown_async(self, *, reason: str) -> bool:
|
||||
"""不可逆关闭混合 Runtime,并返回全部 owner 是否收敛。"""
|
||||
with self._shutdown_lock:
|
||||
self._shutdown = True
|
||||
converged = True
|
||||
for spec in self._registry.list_specs():
|
||||
adapter = self._adapters[spec.kind]
|
||||
if getattr(adapter, "execution_mode", None) is AdapterExecutionMode.ASYNC:
|
||||
await self._stop_async(spec.id, reason=reason, shutdown=True)
|
||||
stopped = await self._stop_async(
|
||||
spec.id,
|
||||
reason=reason,
|
||||
shutdown=True,
|
||||
)
|
||||
else:
|
||||
await asyncio.to_thread(
|
||||
stopped = await asyncio.to_thread(
|
||||
self._stop_sync,
|
||||
spec.id,
|
||||
reason=reason,
|
||||
shutdown=True,
|
||||
)
|
||||
if not stopped:
|
||||
converged = False
|
||||
return converged
|
||||
|
||||
@@ -247,8 +247,9 @@ class HostModuleAdapter:
|
||||
@staticmethod
|
||||
def stop(spec: CapabilitySpec, instance: Any, generation: int) -> None:
|
||||
"""停止实例拥有的资源;Runtime 会先撤销其运行态可见性。"""
|
||||
del spec, generation
|
||||
instance.stop()
|
||||
del generation
|
||||
if instance.stop() is False:
|
||||
raise RuntimeError(f"Host Module {spec.id} 资源未完成收口")
|
||||
|
||||
@staticmethod
|
||||
def cleanup(
|
||||
|
||||
@@ -224,13 +224,17 @@ class ModuleManager(metaclass=Singleton):
|
||||
self._refresh_running_projection()
|
||||
logger.info("所有模块停止完成")
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""进程关闭时不可逆停止 Runtime,阻止并发能力重新发布。"""
|
||||
def shutdown(self) -> bool:
|
||||
"""不可逆停止 Runtime,并返回所有模块 owner 是否收敛。"""
|
||||
logger.info("正在关闭模块运行时...")
|
||||
with self._lifecycle_lock:
|
||||
self._runtime.shutdown(reason="application_shutdown")
|
||||
converged = self._runtime.shutdown(reason="application_shutdown")
|
||||
self._refresh_running_projection()
|
||||
logger.info("模块运行时关闭完成")
|
||||
if converged:
|
||||
logger.info("模块运行时关闭完成")
|
||||
else:
|
||||
logger.error("模块运行时关闭后仍有资源 owner 未收敛")
|
||||
return converged
|
||||
|
||||
def reload(self) -> None:
|
||||
"""保留旧插件可观察的 stop、load、ModuleReload 同步顺序。"""
|
||||
|
||||
@@ -48,8 +48,8 @@ class ManagedResourceRuntime(Protocol):
|
||||
async def stop_async(self, capability_id: str, *, reason: str) -> None:
|
||||
"""通过异步 adapter 停止资源。"""
|
||||
|
||||
async def shutdown_async(self, *, reason: str) -> None:
|
||||
"""关闭混合同步和异步 adapter 的 Runtime。"""
|
||||
async def shutdown_async(self, *, reason: str) -> bool:
|
||||
"""关闭混合 Runtime,并返回全部资源是否收敛。"""
|
||||
|
||||
|
||||
_runtime_lock = threading.RLock()
|
||||
@@ -174,9 +174,9 @@ async def stop_managed_resource_async(capability_id: str, *, reason: str) -> Non
|
||||
raise RuntimeError(f"未知 Managed Resource kind:{kind}")
|
||||
|
||||
|
||||
async def shutdown_managed_resource_runtime(*, reason: str) -> None:
|
||||
"""关闭已配置 Runtime;未配置时直接返回,绝不因关闭而创建资源。"""
|
||||
async def shutdown_managed_resource_runtime(*, reason: str) -> bool:
|
||||
"""关闭已配置 Runtime,未配置时按已收敛处理。"""
|
||||
runtime = _runtime(required=False)
|
||||
if runtime is None:
|
||||
return
|
||||
await runtime.shutdown_async(reason=reason)
|
||||
return True
|
||||
return await runtime.shutdown_async(reason=reason)
|
||||
|
||||
@@ -38,10 +38,10 @@ def init_managed_resources() -> CapabilityRuntime:
|
||||
return _managed_resource_runtime
|
||||
|
||||
|
||||
async def stop_managed_resources() -> None:
|
||||
"""关闭已经初始化的资源 Runtime;未初始化时不执行发现或激活。"""
|
||||
async def stop_managed_resources() -> bool:
|
||||
"""关闭已初始化的资源 Runtime,并返回 owner 是否收敛。"""
|
||||
with _runtime_lock:
|
||||
runtime = _managed_resource_runtime
|
||||
if runtime is None:
|
||||
return
|
||||
await runtime.shutdown_async(reason="application_shutdown")
|
||||
return True
|
||||
return await runtime.shutdown_async(reason="application_shutdown")
|
||||
|
||||
@@ -601,23 +601,36 @@ async def settle_events() -> bool:
|
||||
return await event_manager.drain_async(seal=False)
|
||||
|
||||
|
||||
async def stop_modules():
|
||||
async def stop_modules() -> bool:
|
||||
"""
|
||||
服务关闭
|
||||
关闭模块服务,并返回全部资源 owner 是否收敛。
|
||||
"""
|
||||
async def run_step(name: str, callback: Callable[[], object]) -> bool:
|
||||
"""单个模块资源关闭失败时继续执行后续阶段"""
|
||||
all_converged = True
|
||||
|
||||
async def run_step(
|
||||
name: str,
|
||||
callback: Callable[[], object],
|
||||
*,
|
||||
record_failure: bool = True,
|
||||
) -> bool:
|
||||
"""执行单个关闭步骤,失败时继续收口并保留诚实结果。"""
|
||||
nonlocal all_converged
|
||||
try:
|
||||
result = callback()
|
||||
if inspect.isawaitable(result):
|
||||
await result
|
||||
return True
|
||||
result = await result
|
||||
converged = result is not False
|
||||
if not converged:
|
||||
logger.error("关闭%s未收敛,继续执行后续资源收口", name)
|
||||
except asyncio.CancelledError:
|
||||
logger.warning("关闭%s时收到取消请求,继续执行资源收口", name)
|
||||
return False
|
||||
converged = False
|
||||
except Exception as err:
|
||||
logger.error(f"关闭{name}失败:{err}")
|
||||
return True
|
||||
converged = False
|
||||
if not converged and record_failure:
|
||||
all_converged = False
|
||||
return converged
|
||||
|
||||
await run_step("图片代理安全日志合并器", close_image_proxy_block_log_coalescer)
|
||||
await run_step("模块", lambda: ModuleManager().shutdown())
|
||||
@@ -631,7 +644,9 @@ async def stop_modules():
|
||||
await run_step("异步Redis缓存连接", lambda: AsyncRedisHelper().close())
|
||||
# Web Agent 的取消 finally 可能还要写入最终展示快照,必须先完成任务收尾,再关闭写入准入。
|
||||
web_agent_drained = await run_step(
|
||||
"Web Agent后台任务", shutdown_web_agent_background_tasks
|
||||
"Web Agent后台任务",
|
||||
shutdown_web_agent_background_tasks,
|
||||
record_failure=False,
|
||||
)
|
||||
if not web_agent_drained:
|
||||
web_agent_drained = await run_step(
|
||||
@@ -648,15 +663,18 @@ async def stop_modules():
|
||||
)
|
||||
else:
|
||||
persistence_drained = False
|
||||
all_converged = False
|
||||
logger.error("Web Agent任务未完成收尾,跳过持久化和数据库关闭以保护活动事务")
|
||||
if persistence_drained:
|
||||
await run_step("数据库任务", stop_database_worker)
|
||||
if _database_worker is None:
|
||||
await run_step("数据库连接", close_database)
|
||||
else:
|
||||
all_converged = False
|
||||
logger.error("数据库任务未收敛,跳过数据库连接关闭以避免运行中事务使用已释放连接")
|
||||
await run_step("前端服务", stop_frontend)
|
||||
await run_step("临时文件", clear_temp)
|
||||
return all_converged
|
||||
|
||||
|
||||
async def init_modules() -> HostRuntime:
|
||||
|
||||
Reference in New Issue
Block a user