async_runner.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. """find_agent 异步运行与资源清理。"""
  2. from __future__ import annotations
  3. import asyncio
  4. import logging
  5. import threading
  6. from supply_agent.agent.core import Agent
  7. from supply_agent.types import AgentResult
  8. logger = logging.getLogger(__name__)
  9. _ASYNC_CLEANUP_TIMEOUT_SECONDS = 5.0
  10. async def _drain_event_loop() -> None:
  11. """Bound cleanup of httpx/OpenAI background tasks."""
  12. loop = asyncio.get_running_loop()
  13. pending = {
  14. task
  15. for task in asyncio.all_tasks(loop)
  16. if task is not asyncio.current_task() and not task.done()
  17. }
  18. if not pending:
  19. return
  20. done, still_pending = await asyncio.wait(
  21. pending,
  22. timeout=_ASYNC_CLEANUP_TIMEOUT_SECONDS,
  23. )
  24. for task in done:
  25. if task.cancelled():
  26. continue
  27. error = task.exception()
  28. if error is not None:
  29. logger.debug("pending async task error during drain: %s", error)
  30. if still_pending:
  31. logger.warning(
  32. "Cancelling %d async cleanup task(s) after %.0fs",
  33. len(still_pending),
  34. _ASYNC_CLEANUP_TIMEOUT_SECONDS,
  35. )
  36. for task in still_pending:
  37. task.cancel()
  38. await asyncio.wait(still_pending, timeout=1.0)
  39. async def _close_agent_async_resources(agent: Agent) -> None:
  40. """在事件循环关闭前显式释放异步 HTTP 连接。"""
  41. async_client = getattr(agent.llm, "_async_client", None)
  42. if async_client is not None:
  43. try:
  44. await asyncio.wait_for(
  45. async_client.close(),
  46. timeout=_ASYNC_CLEANUP_TIMEOUT_SECONDS,
  47. )
  48. except TimeoutError:
  49. logger.warning(
  50. "close async llm client timed out after %.0fs",
  51. _ASYNC_CLEANUP_TIMEOUT_SECONDS,
  52. )
  53. except Exception:
  54. logger.debug("close async llm client failed", exc_info=True)
  55. await _drain_event_loop()
  56. async def arun_find_agent(
  57. agent: Agent,
  58. user_input: str,
  59. *,
  60. timeout_seconds: float,
  61. ) -> AgentResult:
  62. """在单个事件循环内运行 find_agent 核心循环并确保资源释放(不含 OSS 发布)。"""
  63. try:
  64. return await asyncio.wait_for(
  65. agent.arun_core(user_input),
  66. timeout=timeout_seconds,
  67. )
  68. except TimeoutError:
  69. logger.error("find_agent timed out after %.0fs", timeout_seconds)
  70. raise TimeoutError(
  71. f"find_agent timed out after {timeout_seconds:.0f}s"
  72. ) from None
  73. finally:
  74. await _close_agent_async_resources(agent)
  75. def _shutdown_worker_loop(loop: asyncio.AbstractEventLoop) -> None:
  76. """Release async generators/executor before closing a worker-thread loop."""
  77. shutdown_timeout = 3.0
  78. try:
  79. loop.run_until_complete(
  80. asyncio.wait_for(loop.shutdown_asyncgens(), timeout=shutdown_timeout)
  81. )
  82. except Exception:
  83. logger.debug("shutdown_asyncgens failed", exc_info=True)
  84. shutdown_executor = getattr(loop, "shutdown_default_executor", None)
  85. if shutdown_executor is not None:
  86. try:
  87. loop.run_until_complete(
  88. asyncio.wait_for(
  89. shutdown_executor(),
  90. timeout=shutdown_timeout,
  91. )
  92. )
  93. except Exception:
  94. logger.debug("shutdown_default_executor failed", exc_info=True)
  95. def _run_coroutine(coro) -> AgentResult:
  96. """同步入口:主线程用 asyncio.run;worker 线程每次新建并关闭独立 loop。"""
  97. try:
  98. asyncio.get_running_loop()
  99. except RuntimeError:
  100. pass
  101. else:
  102. raise RuntimeError("run_find_agent 不能在已运行的事件循环内调用")
  103. if threading.current_thread() is threading.main_thread():
  104. return asyncio.run(coro)
  105. loop = asyncio.new_event_loop()
  106. asyncio.set_event_loop(loop)
  107. try:
  108. return loop.run_until_complete(coro)
  109. finally:
  110. try:
  111. _shutdown_worker_loop(loop)
  112. finally:
  113. loop.close()
  114. asyncio.set_event_loop(None)