Skip to content

fix(core): 健康检查的超时守卫在 race 落定时被清除 (#4875) - #4950

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4875-health-monitor-timeout
Aug 3, 2026
Merged

fix(core): 健康检查的超时守卫在 race 落定时被清除 (#4875)#4950
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4875-health-monitor-timeout

Conversation

@xuyushun441-sys

@xuyushun441-sys xuyushun441-sys commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #4875

#4813(PR #4874)同一种漏法的另一个实例:守卫 armed 之后被扔掉。

改了什么

packages/core/src/health-monitor.ts —— 私有 timeout() 换成私有 helper
raceCheckTimeout(),与 PR #4874packages/core/src/kernel.ts 引入的
raceStartupTimeout() 同形(没有发明第三种写法):

try {
  return await Promise.race([check, timeoutPromise]);
} finally {
  clearTimeout(guard);
}

原来的 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(后者是任何重构都能满足的同义反复):

  1. 检查赢下 race 后不留 ref'd 定时器 —— process.getActiveResourcesInfo() 只报当前钉住
    事件循环
    的资源,正是让 os migrate 空转 120 秒的那个属性。
  2. 连跑 6 轮周期检查不累积守卫 —— fake timers 下 vi.getTimerCount() 计数。这一条能识破
    unref() 式的假修复:fake-timer 计数看得见 unref 掉的定时器。
  3. 检查真的挂住时超时照常上报(Health check timeout after 100ms,状态 failed)—— 回收
    守卫不得等于解除守卫。

前两条在改动前的源码上确认为(expected 2 to be 1),第三条改动前后都绿(它防的是
把修法做成 unref())。

验证

pnpm --filter @objectstack/core test   →  Test Files 28 passed (28) / Tests 465 passed (465)
tsc --noEmit -p packages/core/tsconfig.json  →  health-monitor.{ts,test.ts} 零报错
                                                (该包在 #4311 的 DEBT 台账内,存量报错未增减)
pnpm check:nul-bytes / check:role-word / check:error-code-casing
     / check:release-notes / check:type-check-coverage  →  全绿

顺手发现,未在本 PR 修(Prime Directive #10)

扫了全仓的 Promise.race + setTimeout 守卫点:同形漏法还剩两处生产实例
(service-automationexecuteWithTimeout —— 每个流程节点一根;core/hot-reload.ts
的 destroy 守卫),另有两处用 unref() 代替回收需要维护者判一次语义。已记在 #4952,
不夹带进这个 PR。


🤖 Generated with Claude Code

https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX

…#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
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 5:03pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/core.

24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/automation/webhooks.mdx (via @objectstack/core)
  • content/docs/concepts/north-star.mdx (via packages/core)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core)
  • content/docs/kernel/services.mdx (via @objectstack/core)
  • content/docs/permissions/authentication.mdx (via @objectstack/core)
  • content/docs/permissions/authorization.mdx (via packages/core)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core)
  • content/docs/plugins/index.mdx (via @objectstack/core)
  • content/docs/plugins/packages.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core)
  • content/docs/releases/implementation-status.mdx (via @objectstack/core)
  • content/docs/releases/v12.mdx (via @objectstack/core)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v17.mdx (via @objectstack/core)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HealthMonitor.timeout() 是 #4813 同一个漏法 —— 健康检查的超时守卫赢下 race 后也不清除

2 participants