Prhub

#36673 [CI] Read subprocess stdout on a background thread to avoid EOF deadlock

原始 PR 作者 pllimax 合并时间 2026-08-29 01:11 文件变更 1 提交数 4 评论 1 代码增减 +11 / -3

执行摘要

修复 CI 子进程 stdout 读取 EOF 死锁

PR 描述指出:run_unittest_files 使用阻塞的 for line in process.stdout 循环读取子进程输出,然后才调用 process.wait()。当子进程输出足够多填满管道缓冲区时,该循环可能在 EOF 上无限阻塞,导致死锁。该修复旨在消除 CI 中的潜在挂起。

该 PR 值得快速浏览以了解 CI 死锁修复模式,尤其是后台线程处理子进程输出的技巧。但技术深度有限,不涉及核心推理逻辑,可跳过精读。关注点:join(timeout) 的合理性,以及是否需要在极端情况下处理 stdout 不关闭的问题。

讨论亮点

该 PR 无 review 评论。仅有一条维护者 iforgetmyname 的评论 /tag-run-ci-label 用于触发 CI。审核人 iforgetmyname 直接批准(APPROVED),未提出异议。

实现拆解

  1. 定位问题:在 python/sglang/test/ci/ci_utils.pyrun_one_file 函数(capture_output=True 分支)中,原先用 for line in process.stdout 同步读取输出,再 process.wait(),存在 EOF 死锁风险。
  2. 引入后台读取线程:新增内部函数 read_output,将逐行读取循环移入其中,并通过 threading.Thread(target=read_output, daemon=True) 创建后台守护线程并启动。
  3. 调整主流程:先 process.wait() 等待子进程结束,再 reader_thread.join(timeout=60) 限时等待读取线程收尾,确保主线程不会无限阻塞。
  4. 配套影响:仅改动 CI 测试工具函数,未涉及配置、部署或测试用例本身;由于是死锁修复,未新增单元测试,依赖现有 CI 流程验证。
文件 模块 状态 重要度
python/sglang/test/ci/ci_utils.py CI 工具 modified 5.29

关键符号

read_output

关键源码片段

python/sglang/test/ci/ci_utils.py test-coverage

核心变更文件:修复 CI 子进程 stdout 读取死锁,将阻塞读取移至后台线程并超时 join。

# python/sglang/test/ci/ci_utils.pydef run_one_file(filename, capture_output=False):
    nonlocal process, output_lines
    # ... 省略前置代码 ...
    if capture_output:
        # 创建子进程,合并 stdout/stderr 到管道
        process = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            errors="ignore", # 忽略非 UTF-8 字节,防止 UnicodeDecodeError
        )
        output_lines = []
​
        # 后台线程持续读取 stdout,避免主线程在管道缓冲满时陷入 EOF 死锁。
        def read_output():
            for line in process.stdout:
                logger.info(line.rstrip())
                output_lines.append(line)
​
        # 使用 daemon 线程,确保即使主线程异常退出,线程也不会阻止进程结束。
        reader_thread = threading.Thread(target=read_output, daemon=True)
        reader_thread.start()
        process.wait() # 等待子进程结束
        # 限时等待读取线程收尾,最多 60 秒,防止线程永久阻塞。
        reader_thread.join(timeout=60)
    else:
        process = subprocess.Popen(cmd, stdout=None, stderr=None)
        process.wait()
    # ... 省略后续代码 ...

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 读取不完整风险reader_thread.join(timeout=60) 若超时,可能丢弃子进程部分输出,影响重试决策和日志完整性,但概率低(正常子进程结束后 stdout 应立即 EOF)。
  2. 线程安全output_lines 由后台线程追加,主线程仅在 join 后读取,无并发访问,但若未来主线程在 join 前访问则有风险。
  3. 死锁消除不彻底:若 process.stdout 在子进程结束后仍不 EOF(极端情况),连接线程仍可能阻塞在 read_output 内,但 daemon 线程不会阻止进程退出。
  4. 影响范围:仅 CI 工具,不涉及生产路径,风险低。

影响用户/开发者:CI 稳定性提升,避免因死锁导致的挂起和超时,减少 CI 重试。影响系统:改善 run_unittest_files 在输出较大时的可靠性。影响团队:维护者需了解后台线程机制,但代码改动小,易于理解。影响程度较小,限于 CI 测试运行器。

CI 稳定性 后台线程超时可能丢输出

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论