async_runner.py 4.2 KB

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