fix(core): 健康检查的超时守卫在 race 落定时被清除 (#4875) - #4950
Merged
Merged
Conversation
…#4875) `PluginHealthMonitor.performHealthCheck()` armed its timeout guard via the private `timeout()` helper and then abandoned it: when the plugin's `checkMethod` won the race, the `setTimeout` stayed ref'd in the event loop for the full `config.timeout`. Same leak as the kernel's init/start guards (#4813, PR #4874), with one aggravating difference — health checks are periodic, so the orphans accumulate one per plugin per round instead of being a fixed cost paid once at boot. Replaces `timeout()` with `raceCheckTimeout()`, the same shape and the same reasoning as `ObjectKernel.raceStartupTimeout()`: `try { await Promise.race(...) } finally { clearTimeout(guard) }`. Not `unref()` — an unref'd guard stops pinning the loop but also stops being a guard, so a check that never settles is silently dropped instead of reported. Regression tests: no ref'd timer survives a check that wins the race, no guard accumulates across periodic rounds (counted under fake timers, which also sees an `unref()`'d timer and so rejects a fake fix), and the timeout is still reported when the check genuinely hangs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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 #4875
与 #4813(PR #4874)同一种漏法的另一个实例:守卫 armed 之后被扔掉。
改了什么
packages/core/src/health-monitor.ts—— 私有timeout()换成私有 helperraceCheckTimeout(),与 PR #4874 在packages/core/src/kernel.ts引入的raceStartupTimeout()同形(没有发明第三种写法):原来的
timeout()只此一处调用,一并删除。取值(config.timeout)一个都没动 —— 问题从来不在时长,而在没人回收。
为什么这条比 #4813 更值得修,尽管今天看不到现象
#4813 的现象(一次性 CLI 进程空转 120 秒)在这里观察不到,原因只是
startMonitoring()目前没有任何调用点在内核启动流程里。但机制一旦发作会更糟:内核那两处是启动时一次性的
固定份额(4 个插件 = 8 根),健康检查是周期性的 —— 每个插件每一轮各留一根,
interval越密、
timeout越长堆得越高。为什么是
clearTimeout而不是unref()沿用 #4874 评审已经记下的结论:
unref()让定时器不再钉住事件循环的同时,也让它不再是一个守卫 —— 若检查永不 settle 且没有别的东西撑着事件循环,Node 会在定时器触发之前退出,超时被
静默吞掉。守卫必须在 race 未决期间保持 ref'd、在落定那一刻被回收。
测试(
packages/core/src/health-monitor.test.ts,+3)断言的是可观察后果,不是源码里有没有
clearTimeout(后者是任何重构都能满足的同义反复):process.getActiveResourcesInfo()只报当前钉住事件循环的资源,正是让
os migrate空转 120 秒的那个属性。vi.getTimerCount()计数。这一条能识破unref()式的假修复:fake-timer 计数看得见 unref 掉的定时器。Health check timeout after 100ms,状态failed)—— 回收守卫不得等于解除守卫。
前两条在改动前的源码上确认为红(
expected 2 to be 1),第三条改动前后都绿(它防的是把修法做成
unref())。验证
顺手发现,未在本 PR 修(Prime Directive #10)
扫了全仓的
Promise.race+setTimeout守卫点:同形漏法还剩两处生产实例(
service-automation的executeWithTimeout—— 每个流程节点一根;core/hot-reload.ts的 destroy 守卫),另有两处用
unref()代替回收需要维护者判一次语义。已记在 #4952,不夹带进这个 PR。
🤖 Generated with Claude Code
https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX