refactor: unify capability shutdown convergence

This commit is contained in:
jxxghp
2026-08-24 13:48:11 +08:00
parent 2aa41ea8fe
commit 009631b8ee
18 changed files with 330 additions and 70 deletions
+4 -9
View File
@@ -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",
)
+7 -3
View File
@@ -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]]:
"""
+20 -8
View File
@@ -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
+40 -13
View File
@@ -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(
+8 -4
View File
@@ -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 同步顺序。"""
+6 -6
View File
@@ -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")
+27 -9
View File
@@ -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: