Async SSE client reuses httpx.AsyncClient across event loops -> "Event loop is closed"

未关闭 适合新手
#507 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
2/5
预计耗时
1-3 小时
新手友好度
78/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
python
领域
api

调研方向

从 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 内容生成。

描述

bug

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

贡献指南

这个仓库没有索引到贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

conductor-oss/python-sdk 的其他 Issue

查看 conductor-oss/python-sdk 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。