Skip to content

fix(provider): 构建期发心跳,避免冷机首次被 d2x 活性超时误杀 - #88

Merged
Sunrisepeak merged 1 commit into
mainfrom
fix/provider-build-heartbeat
Aug 2, 2026
Merged

fix(provider): 构建期发心跳,避免冷机首次被 d2x 活性超时误杀#88
Sunrisepeak merged 1 commit into
mainfrom
fix/provider-build-heartbeat

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

接着 #87 继续排查「d2x checker 在 Windows 10 上卡住」的反馈。#87 修的是 cmd.exe
/dev/null 重定向(那条更早、更普遍);这条与平台无关,冷机第一次才撞得到

实测结论:构建期完全静默

mcpp test --message-format json   →  整个构建期一个字节都不产出

不是推断,是跑出来的 —— 带不带 -q 都一样:stdout 只有末尾那两行 JSON,
stderr 全空(机器可读模式把人读输出整个收编进 JSON 了)。

所以从 d2x 的视角看,Provider 从 stage("compile") 之后就彻底沉默,直到构建结束:

{"event":"stage","name":"compile"}
        ……  整个 mcpp test 期间:零字节  ……
{"event":"verdict",...}

为什么这会表现成「卡住」

冷机第一次要在这段沉默里备工具链与 std 模块。一旦超过 d2x 的活性超时
(provider_idle_timeout,默认 120s 且默认开启),正常构建会被当成挂死而终止

更糟的是它不是「报错一次就完了」:checker 判 fail 后驻留等文件变更,学习者改一次
文件就重试一次、每次都在同一处被杀 —— 循环无解,与「卡住」难以区分。

做法:心跳,而不是调大阈值

capture_stdout 把读取交给工作线程(fgets 是阻塞的,单线程在沉默期根本回不到
我们手里),主线程每 20s 发一条 output 事件。一举两得:

  • 持续喂活 d2x 的活性计时器 —— 任何字节都会重置它
  • 让学习者看见首次构建正在进行,而不是对着黑屏怀疑卡死

比单纯把 120s 调大更对症:阈值调多大都是猜,心跳把「有进展」变成可观测事实。

runner 保持协议无关 —— 回调由 main.cpp 注入,发射逻辑不下沉到 runner。
不传回调时完全不介入(默认参数为空,直接 join,无轮询)。

验证

  • 心跳确实会发:临时把间隔设成 0 跑一次,确认 output 事件出现;改回 20s 后
    热构建 0 条 —— 不打扰正常路径(热构建 60ms,远不到 20s)

  • 无回归:bash d2x/buildtools/tests/e2e.sh zh

    E2E(provider) 协议冒烟: 52 个练习,check 事件流完整 ✓
    E2E(zh) pristine: 全部练习保持未通过 ✓
    E2E(zh) solutions: 52/52 参考答案全部通过 ✓
    E2E: ALL GREEN
    

未覆盖

本机无法低成本复现真正的冷启动 —— 昂贵的部分在全局 ~/.mcpp/build-cache/,
清工程的 target/ 只会得到 60ms 的重建。所以「超过 120s 的沉默」这一前提是从
「构建期零输出」+「冷机需备工具链与 std 模块」推出来的,心跳逻辑本身则是实测过的。

有反馈说 d2x checker 在 Windows 10 上会卡住。除了已修的 cmd.exe 重定向,还有
一条与平台无关、冷机才撞得到的:

  mcpp test --message-format json 在整个构建期一个字节都不产出。

这是实测结论,不是推断 —— 带不带 -q 都一样:stdout 只有末尾那两行 JSON,
stderr 全空(机器可读模式把人读输出整个收编进 JSON 了)。所以从 d2x 的视角看,
Provider 从 stage("compile") 之后就彻底沉默,直到构建结束。

冷机第一次要在这段沉默里备工具链与 std 模块。一旦超过 d2x 的活性超时
(provider_idle_timeout,默认 120s),正常构建会被当成挂死而终止;更糟的是学习者
改一次文件就重试一次、每次都在同一处被杀,表现为「怎么改都过不去」——
与「卡住」难以区分。

做法:capture_stdout 把读取交给工作线程(fgets 是阻塞的,单线程在沉默期根本回
不到我们手里),主线程每 20s 发一条 output 事件。一举两得:持续喂活 d2x 的计时
器,并让学习者看见首次构建正在进行,而不是对着黑屏干等。

比单纯调大 120s 阈值更对症 —— 阈值调多大都是猜,心跳把「有进展」变成可观测
事实。runner 保持协议无关:回调由 main.cpp 注入,发射逻辑不下沉。

验证:
- 心跳会发 —— 临时把间隔设为 0 跑一次,确认 output 事件出现(改回 20s 后
  热构建 0 条,不打扰正常路径)
- 无回归 —— d2x/buildtools/tests/e2e.sh zh:协议冒烟 52 练习 ✓ /
  pristine 全部保持未通过 ✓ / 52/52 参考答案全部通过 ✓
@Sunrisepeak
Sunrisepeak merged commit 32a7a3e into main Aug 2, 2026
1 check passed
@Sunrisepeak
Sunrisepeak deleted the fix/provider-build-heartbeat branch August 2, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant