
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(端口分配行为可能略有不同)
解决步骤
- 将以下五个测试文件中的端口选择固件迁移到使用 existing
run_uvicorn_in_thread辅助函数(在tests/test_helpers.py:15):tests/shared/test_streamable_http.pytests/shared/test_sse.pytests/server/test_sse_security.pytests/server/test_streamable_http_security.pytests/client/test_http_unicode.py
- 具体修改:将旧的固件模式(先 pick_free_port 再关闭 socket,然后启动服务器)替换为直接使用
run_uvicorn_in_thread,该函数在绑定和监听 socket 后才交给 uvicorn,没有窗口期。 - 修改后示例(来自 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 - 确保
run_uvicorn_in_thread返回的 URL 来自绑定好的 socket:在 helper 内部,socket 绑定并监听端口 0,然后从sock.getsockname()读取端口。 - 移除旧的
pick_free_port辅助函数和wait_for_server轮询辅助函数,防止未来被错误复用。 - 如果修复只合入主分支,需要同时向后移植到 v1.x 分支。
验证方法
在修复后的版本上,使用 pytest -n auto 运行多次(特别是 CI 环境下的矩阵测试),观察是否还会出现上述两种失败签名之一的错误。可以运行 pytest tests/shared/test_streamable_http.py -n auto --count 10(如果 pytest-repeat 插件可用)来验证稳定性。



