test(core): 让 #4875 不变量钉住 monitor 自己的 guard,而不是进程全局 ref'd timer 计数 (#6329) - #6582
Merged
Conversation
…6329) `leaves no ref'd timer behind when the health check wins the race` 此前用 `process.getActiveResourcesInfo()` 的进程全局 ref'd Timeout 计数做「await 前后 比对」。该计数是全进程共享的:vitest runner 自己就在同一个事件循环上挂着一个 **没有 unref 的 100ms** 定时器(`throttle(sendTasksUpdate, 100)`,@vitest/runner), 而它的 per-test 超时守卫反而显式 `timer.unref?.()`、根本不计入。于是合并队列全量并发 把两次读数之间的窗口拉长到 100ms 以上(失败那次实测 105ms)时,runner 的节流定时器 在窗口内到期,计数凭空少 1,断言读到 `expected +0 to be 1`——与 monitor 无关。 改为:用一次性的 `setTimeout` 记录取回 monitor 本轮真正armed 的 guard 句柄(按配置的 timeout 值与循环上其它定时器区分),随后所有读数都放在**同一个同步回合**里相邻取, 两条语句之间不可能有任何定时器回调运行,因此差值只可能是 monitor 自己造成的。 断言强度不变(反向验证):去掉 finally 里的 clearTimeout ⇒ 本用例与 fake-timer 同伴用例双红;把 clearTimeout 换成 arm 时 unref ⇒ 本用例绿、fake-timer 同伴红, 与改动前的分工完全一致。新增 `expect(guards).toHaveLength(1)` 防止「什么都没测到」 的空绿。生产面零改动。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…lth-monitor-flake
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
baozhoutao
marked this pull request as ready for review
August 8, 2026 06:07
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6329
leaves no ref'd timer behind when the health check wins the race(#4875 不变量 pin)在合并队列全量并发跑间歇红。本 PR 让该用例钉住 monitor 自己 armed 的 guard 句柄,而不是进程全局的 ref'd timer 计数。生产面零改动(health-monitor.ts未被触碰),test-only。前提复核(动手前逐条实测)
health-monitor.test.ts:124-125的refdTimers()=process.getActiveResourcesInfo().filter(r => r === 'Timeout').length;:131取before,:144expect(refdTimers()).toBe(before)。(注:origin/main 的类型已是PluginHealthCheckParsed,issue 正文写的PluginHealthCheck是旧名,形状本身不变)成因实证 —— 不是假设,是量出来的
扰动源已定位到
@vitest/runner自身,不是"其他并发用例"的笼统猜测:async_hooks追踪该进程内所有活着的Timeout的创建栈,before快照点实测before=1、all=["PipeWrap","PipeWrap","PipeWrap","PipeWrap","Timeout"]—— 与 CI 签名的before是 1 完全一致;活着的 timer 栈全部指向@vitest/runner。runWithTimeout里const timer = setTimeout(...); timer.unref?.();—— runner 的 per-test 超时守卫显式 unref,所以它根本不进getActiveResourcesInfo(),可以排除;const sendTasksUpdateThrottled = throttle(sendTasksUpdate, 100),而throttle内部pendingCall ??= setTimeout(() => call.bind(this)(...args), ms)—— 这个 100ms 的节流定时器没有 unref,它就是被计入的那个 1。于是成因是确定的:两次读数之间隔着
await,窗口一旦超过 100ms,runner 的节流定时器就在窗口内到期、且来不及被重新 armed,全局计数凭空少 1。issue 记录的失败用例耗时正是 105ms —— 刚好越过 100ms 节流线,这个数字不是巧合。并发注入复现(反向验证 B):在同一循环上注入一个 25ms 的无关 ref'd timer,并把窗口拉到 60ms(模拟满载),旧形状复现同族失败:
与 CI 的
expected +0 to be 1@105ms 同族;计数从 1 变 2 只是因为我额外注入了一个,机制一致。同一注入下新形状绿。选型
正文给了两个候选,选前者(精确计数 monitor 自身的 timer),并且不需要动
health-monitor.ts:setTimeout记录取回本轮 armed 的 guard 句柄,按配置的timeout值(120_000)与循环上其它定时器区分。不引入生产侧的 timer 工厂 / 计数钩子注入面 —— 那是纯为测试而扩的公开面,没有业务拉动,而测试侧记录能拿到同样的归属信息,生产行为零变化。clearTimeout被调用过 —— 保留了原注释坚持的 "asserts the observable consequence, never the source"。新增
expect(guards).toHaveLength(1):防止 guard 根本没 armed 时,后面的差值因为"什么都没测到"而空绿(PR #5046 那类假绿)。另保留expect(whileMonitoring - afterStop).toBe(1)作为仪器自检 —— 证明refdTimers()确实观察得到本 monitor 挂在本循环上的 timer。反向验证(方向先写死,再测)
raceCheckTimeout的finally { clearTimeout(guard) }:212 expected 1 to be 2(afterStop=2,测试补刀回收后掉到 1)、:272 expected 2 to be 1,Tests 2 failed | 5 passedguard.unref?.():272 expected 2 to be 1,Tests 1 failed | 6 passedA' 值得单独说明:
unref反面修法在改动前后都是本用例绿、fake-timer 同伴红 —— 后者靠vi.getTimerCount()仍能看见 unref'd timer 来区分"回收了"与"只是从循环上摘掉了"(原注释 :200-202 已写明)。本 PR 没有改变这个分工。稳定性跑数
packages/core全量套件连跑 20 次(flake 只在全量并发下才有邻居任务的节流定时器,单文件跑复现不出来):命令输出
changeset
test-only,无 changeset(
health-monitor.ts未改,不发布任何东西)。需要skip-changeset标签 —— 按本单派发口径由 PM 落标,开发席不动标签。Generated by Claude Code