Async SSE client reuses httpx.AsyncClient across event loops -> "Event loop is closed"
还没有人认领这个 Issue。
评估
调研方向
从 src/conductor/client/orkes/orkes_agent_client.py 中的 OrkesAgentClient._get_sse_async_client() 开始,然后针对本地 keep-alive 服务器运行 issue 的 two-asyncio.run 复现。完成标准是 async client 不会在多个事件循环之间重复使用,并且两次调用都返回 200,且不会出现 RuntimeError: Event loop is closed。
由索引模型根据 Issue 内容生成。
描述
Re-scoped from conductor-oss/conductor#1563 (originally agentspan-ai/agentspan#318). That one was filed against the server repo by the Agentspan migration, but the code actually lives here in the Python SDK.
OrkesAgentClient._get_sse_async_client() (src/conductor/client/orkes/orkes_agent_client.py:274) caches one httpx.AsyncClient and only recreates it when is_closed is true. The catch: a client whose connection pool is bound to a closed event loop is not is_closed -- so it gets reused. Drive the async streaming API through a fresh asyncio.run(...) twice in one process (each asyncio.run spins up and tears down its own loop) and the second call dies with RuntimeError: Event loop is closed. The client is only reset in shutdown_async(), never per-stream.
Good news up front: the original repro (sync start() called twice) no longer reproduces -- the sync SSE path was rewritten on the synchronous requests library, so it never touches an async client. This is confined to the async API (stream_async() / _get_sse_async_client()).
Repro that drives the real getter (a tiny local keep-alive server is all it needs so a connection actually gets pooled and bound to loop A):
import asyncio, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from conductor.client.configuration.configuration import Configuration
from conductor.client.orkes.orkes_agent_client import OrkesAgentClient
class H(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1" # keep-alive so httpx pools the connection
def do_GET(self):
b = b"ok"; self.send_response(200)
self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
def log_message(self, *a): pass
srv = HTTPServer(("127.0.0.1", 0), H); port = srv.server_address[1]
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{port}/"
client = OrkesAgentClient(Configuration(server_api_url="http://localhost:8080/api"))
async def touch():
c = client._get_sse_async_client()
print("is_closed before use:", c.is_closed)
print("GET ->", (await c.get(url)).status_code)
asyncio.run(touch()) # loop A -- opens + pools a connection bound to loop A
asyncio.run(touch()) # loop B -- reuses the cached client -> boom
Output:
is_closed before use: False
GET -> 200
is_closed before use: False
RuntimeError: Event loop is closed
Fix: in _get_sse_async_client(), also recreate the client when the current running loop differs from the one it was created on (track id(asyncio.get_running_loop()) next to the cached client). Confirmed locally that this makes the repro pass (both calls return 200).
- 主要语言
- Python
- 星标
- 104
- 派生
- 43
- 平均合并
- 1 天 13 小时
- 30 天内合并 PR
- 4
贡献指南
这个仓库没有索引到贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
conductor-oss/python-sdk 的其他 Issue
-
难度 1/5 1-3 小时 新手友好度 90/100
conductor-oss/python-sdk#502 ·
-
bug dependencies
难度 2/5 1-3 小时 新手友好度 88/100
conductor-oss/python-sdk#486 ·
-
bug
难度 2/5 1-3 小时 新手友好度 78/100
conductor-oss/python-sdk#483 ·
-
bug
难度 2/5 1-3 小时 新手友好度 82/100
conductor-oss/python-sdk#478 ·
-
bug
难度 2/5 1-3 小时 新手友好度 88/100
conductor-oss/python-sdk#430 ·
查看 conductor-oss/python-sdk 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 88/100
-
难度 2/5 1-3 小时 新手友好度 82/100
-
难度 2/5 1-3 小时 新手友好度 78/100
-
enhancement
难度 2/5 1-3 小时 新手友好度 72/100
-
难度 2/5 1-3 小时 新手友好度 74/100