Flaky streamable-HTTP/SSE tests: TOCTOU port race under pytest -n auto

该问题发生在运行 MCP Python SDK 测试时,尤其是在 CI 环境下使用 pytest -n auto 进行并行测试时。问题与特定 Python 版本无关,属于非确定性间歇性失败。

Flaky streamable-HTTP/SSE tests: TOCTOU port race under pytest -n auto

Flaky streamable-HTTP/SSE tests: TOCTOU port race under pytest -n auto

快速结论:当使用 pytest -n auto 并行运行 MCP Python SDK 测试时,测试服务器选择空闲端口的模式存在竞态条件(time-of-check/time-of-use,TOCTOU),导致不同测试的服务器争夺同一个端口。优先检查是否使用了旧的端口分配模式,并迁移到已修复的 run_uvicorn_in_thread 辅助函数。

问题场景

该问题发生在运行 MCP Python SDK 测试时,尤其是在 CI 环境下使用 pytest -n auto 进行并行测试时。问题与特定 Python 版本无关,属于非确定性间歇性失败。

报错原文

ERROR: [Errno 98] error while attempting to bind on address ('127.0.0.1', 35105): address already in use
AssertionError: assert 2 == 10

pydantic_core.ValidationError: 1 validation error for CallToolResult
content
  Field required [type=missing, input_value={'tools': [...]}, input_type=dict]

原因分析

测试固件(fixture)中端口选择模式存在 TOCTOU 竞态条件:固件先获取一个空闲端口,在 with socket.socket() as s: 块结束后关闭该 socket(释放端口),然后才在实际服务器进程中绑定该端口。在此期间,另一个 xdist worker 的固件可能被 OS 分配相同的端口,导致两个服务器争夺端口。一个会因 Errno 98 绑定失败,另一个会导致客户端连接到错误的服务器实例。

环境排查

  • Python 版本(如 3.9、3.10、3.11、3.12)
  • pytest 版本和 xdist 版本
  • 测试运行参数:是否使用了 -n auto 或其他并行参数
  • 操作系统,特别是 Linux 和 macOS(端口分配行为可能略有不同)

解决步骤

  1. 将以下五个测试文件中的端口选择固件迁移到使用 existing run_uvicorn_in_thread 辅助函数(在 tests/test_helpers.py:15):
    • tests/shared/test_streamable_http.py
    • tests/shared/test_sse.py
    • tests/server/test_sse_security.py
    • tests/server/test_streamable_http_security.py
    • tests/client/test_http_unicode.py
  2. 具体修改:将旧的固件模式(先 pick_free_port 再关闭 socket,然后启动服务器)替换为直接使用 run_uvicorn_in_thread,该函数在绑定和监听 socket 后才交给 uvicorn,没有窗口期。
  3. 修改后示例(来自 Issue 中的 proposed fix):
    # 旧模式(易崩溃):
    @pytest.fixture
    def basic_server_port() -> int:
        with socket.socket() as s:
            s.bind(("127.0.0.1", 0))
            return s.getsockname()[1]
    
    # 新模式(修复版本):
    @pytest.fixture
    def basic_server_url() -> Generator[str, None, None]:
        app = create_app()
        with run_uvicorn_in_thread(app, limit_concurrency=10, timeout_keep_alive=5, access_log=False) as url:
            yield url
  4. 确保 run_uvicorn_in_thread 返回的 URL 来自绑定好的 socket:在 helper 内部,socket 绑定并监听端口 0,然后从 sock.getsockname() 读取端口。
  5. 移除旧的 pick_free_port 辅助函数和 wait_for_server 轮询辅助函数,防止未来被错误复用。
  6. 如果修复只合入主分支,需要同时向后移植到 v1.x 分支。

验证方法

在修复后的版本上,使用 pytest -n auto 运行多次(特别是 CI 环境下的矩阵测试),观察是否还会出现上述两种失败签名之一的错误。可以运行 pytest tests/shared/test_streamable_http.py -n auto --count 10(如果 pytest-repeat 插件可用)来验证稳定性。

参考来源

modelcontextprotocol/python-sdk #2704

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

celebrityanime
celebrityanime
文章: 7799

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注