mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 19:37:54 +08:00
175 lines
7.3 KiB
Python
175 lines
7.3 KiB
Python
"""
|
||
Agent 管理器启动、关闭与空闲回收生命周期。
|
||
"""
|
||
|
||
import asyncio
|
||
|
||
from app.agent.session import AgentManagerUnavailableError, AgentSessionOwner
|
||
from app.application.agent import AgentDataContext
|
||
from app.runtime.log import logger
|
||
|
||
|
||
class AgentLifecycleOwner(AgentSessionOwner):
|
||
"""
|
||
管理 Agent 服务代际、接收门禁与有界关闭。
|
||
"""
|
||
|
||
def configure_data_context(self, data: AgentDataContext) -> None:
|
||
"""
|
||
在服务启动前绑定唯一数据上下文并配置记忆服务。
|
||
"""
|
||
if self._data is data:
|
||
return
|
||
if self._accepting_tasks or self.active_agents:
|
||
raise RuntimeError("AgentManager 已运行,不能替换数据上下文")
|
||
self._data = data
|
||
self._memory.configure(data.chat, data.chat_persistence)
|
||
|
||
async def initialize(self) -> None:
|
||
"""
|
||
初始化管理器
|
||
"""
|
||
async with self._lifecycle_lock:
|
||
if self._accepting_tasks:
|
||
return
|
||
if self._close_finalizer_task and not self._close_finalizer_task.done():
|
||
raise AgentManagerUnavailableError("AgentManager 仍在完成上一代关闭")
|
||
self._memory.initialize()
|
||
if not self._idle_cleanup_task or self._idle_cleanup_task.done():
|
||
self._idle_cleanup_task = asyncio.create_task(
|
||
self._cleanup_idle_sessions()
|
||
)
|
||
self._accepting_tasks = True
|
||
self._closed = False
|
||
|
||
async def close(self) -> bool:
|
||
"""
|
||
关闭管理器,并诚实返回全部会话 owner 是否已经收敛。
|
||
"""
|
||
async with self._lifecycle_lock:
|
||
if self._closed:
|
||
return True
|
||
if self._close_finalizer_task and not self._close_finalizer_task.done():
|
||
return False
|
||
# 门禁必须先关闭;锁内完成清理可阻止等待中的请求在收口期间重新入队。
|
||
self._accepting_tasks = False
|
||
# 子代理提交门禁同样必须在第一次 await 前关闭,否则 detached task
|
||
# 可趁 idle-cleanup 收尾窗口继续创建不属于新生命周期的任务。
|
||
for agent in self.active_agents.values():
|
||
begin_shutdown = getattr(agent, "begin_shutdown", None)
|
||
if callable(begin_shutdown):
|
||
begin_shutdown()
|
||
if self._idle_cleanup_task:
|
||
self._idle_cleanup_task.cancel()
|
||
try:
|
||
await self._idle_cleanup_task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
self._idle_cleanup_task = None
|
||
# 先取消所有 worker,再以有限等待收口,避免关闭阶段无限挂起。
|
||
workers = list(self._session_workers.items())
|
||
for session_id, task in workers:
|
||
self._session_cancel_requested.add(session_id)
|
||
task.cancel()
|
||
timed_out_workers = []
|
||
for session_id, task in workers:
|
||
stopped = await self._wait_for_worker_shutdown(
|
||
session_id,
|
||
task,
|
||
reason="manager_close",
|
||
)
|
||
if not stopped:
|
||
timed_out_workers.append((session_id, task))
|
||
for queue in list(self._session_queues.values()):
|
||
self._discard_queued_messages(
|
||
queue,
|
||
error=AgentManagerUnavailableError("AgentManager 已关闭"),
|
||
)
|
||
self._session_queues.clear()
|
||
self._session_last_used.clear()
|
||
self._session_queue_rejections.clear()
|
||
self._session_last_queue_wait_ms.clear()
|
||
|
||
if timed_out_workers:
|
||
timed_out_session_ids = {
|
||
session_id for session_id, _ in timed_out_workers
|
||
}
|
||
for session_id, task in workers:
|
||
if session_id in timed_out_session_ids:
|
||
continue
|
||
if self._session_workers.get(session_id) is task:
|
||
self._session_workers.pop(session_id, None)
|
||
for session_id, agent in list(self.active_agents.items()):
|
||
if session_id not in timed_out_session_ids:
|
||
if await agent.cleanup() is not False:
|
||
self.active_agents.pop(session_id, None)
|
||
logger.error(
|
||
"AgentManager 关闭时仍有 worker 未收敛,"
|
||
f"保留 {len(timed_out_workers)} 个会话资源直到 worker 结束"
|
||
)
|
||
self._close_finalizer_task = asyncio.create_task(
|
||
self._finish_deferred_close(timed_out_workers)
|
||
)
|
||
return False
|
||
|
||
self._session_workers.clear()
|
||
for session_id, agent in list(self.active_agents.items()):
|
||
if await agent.cleanup() is not False:
|
||
self.active_agents.pop(session_id, None)
|
||
if self.active_agents:
|
||
logger.error(
|
||
"AgentManager 仍有 %d 个 detached 子代理 owner 未收敛",
|
||
len(self.active_agents),
|
||
)
|
||
return False
|
||
await self._memory.close()
|
||
self._closed = True
|
||
return True
|
||
|
||
async def _cleanup_idle_sessions(self) -> None:
|
||
"""
|
||
周期性清理长时间没有新消息的 Agent 会话,避免长期运行后实例持续累积。
|
||
"""
|
||
while True:
|
||
try:
|
||
await asyncio.sleep(self._idle_cleanup_interval)
|
||
for session_id, user_id in self._expired_idle_sessions():
|
||
await self.clear_session(session_id=session_id, user_id=user_id)
|
||
logger.info(f"已清理空闲Agent会话: session_id={session_id}")
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"清理空闲Agent会话失败: {e}")
|
||
|
||
async def _finish_deferred_close(
|
||
self,
|
||
workers: list[tuple[str, asyncio.Task[None]]],
|
||
) -> None:
|
||
"""
|
||
关闭超时后等待遗留 worker,再释放共享 Agent 资源。
|
||
"""
|
||
try:
|
||
await asyncio.gather(
|
||
*(worker for _, worker in workers),
|
||
return_exceptions=True,
|
||
)
|
||
async with self._lifecycle_lock:
|
||
for session_id, worker in workers:
|
||
if self._session_workers.get(session_id) is worker:
|
||
self._session_workers.pop(session_id, None)
|
||
for session_id, agent in list(self.active_agents.items()):
|
||
if await agent.cleanup() is not False:
|
||
self.active_agents.pop(session_id, None)
|
||
self._session_shutdown_pending.clear()
|
||
self._session_cancel_requested.clear()
|
||
if self.active_agents:
|
||
logger.error(
|
||
"AgentManager 延迟关闭后仍有 %d 个子代理 owner 未收敛",
|
||
len(self.active_agents),
|
||
)
|
||
return
|
||
await self._memory.close()
|
||
self._closed = True
|
||
finally:
|
||
self._close_finalizer_task = None
|