Skip to content

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

Closed

Description

@os-zhuang

发现自 #4813 的实现过程(PR #4874)。未认领。 与 #4813 是同一种形状的不同实例。

位置

packages/core/src/health-monitor.ts:

  /**
   * Timeout helper
   */
  private timeout< T >(ms: number, message: string): Promise< T > {
    return new Promise((_, reject) => {
      setTimeout(() => reject(new Error(message)), ms);   // ← 从不 clearTimeout,也不 unref
    });
  }

调用点在 :112,健康检查那条 race:

        this.timeout(config.timeout, `Health check timeout after ${config.timeout}ms`)

与 #4813 修掉的两处一字不差:守卫 armed 之后就被扔掉,健康检查赢下 race 时那根定时器仍带着 ref 挂满 config.timeout。

与 #4813 的差别:今天不发作,但机制相同

#4813 的现象(一次性 CLI 进程空转 120 秒)这里观察不到,原因只是健康监控不在 bootstrap 路径上启动 —— startMonitoring() 没有任何调用点在内核启动流程里,所以 os migrate 一类的进程根本不会 armed 这根定时器。

也就是说这条今天是潜伏的:一旦健康监控真的被接进某个宿主(尤其是周期性检查,每轮一根),它就会变成 #4813 的翻版 —— 而且比 #4813 更糟,因为周期性调用会让孤儿定时器持续累积,不是启动时固定的 8 根。

建议的修法

用 #4874 引入的同一个私有 helper 收编,而不是在 health-monitor.ts 里再写第三种写法:

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

⚠️ 注意别用 unref() —— #4874 实测确认那不是等价方案:unref'd 的守卫在「被守卫的操作永不 settle 且没有别的东西撑着事件循环」时根本不会触发,超时被静默吞掉。守卫必须在 race 未决期间保持 ref'd,在落定的那一刻被回收。理由完整写在 ObjectKernel.raceStartupTimeout() 的 doc comment 里。

helper 目前是 ObjectKernel 的私有方法。收编这条需要先决定它落在哪:提成 packages/core/src/utils/ 下的共享函数,还是 HealthMonitor 各自持有一份。倾向前者 —— 这已经是仓里第三个同形状的超时守卫,再复制一次就该有人把它抽出来了。

边界

Refs #4813(PR #4874)。

Activity

  1. claude commented on Aug 3, 2026

    @claude
    Contributor

    🔒 认领:PM 循环第 1 轮扩批(维护者要求加速,batch 扩至 5)
    会话:session_01NrmBxj8rK2uGCnh9aipjwX(账号 xuyushun441-sys)
    分支:claude/issue-4875-health-monitor-timeout
    Worktree:objectstack-issue-4875
    域:domain:engine
    文件面:packages/core/src/health-monitor.ts 及同目录测试

    ⛔ 硬约束:packages/spec/** 及生成物零改动;metadata-protocol/src/protocol.ts 零改动(在飞 #4721 占用)。
    全局在飞不相交已核:#4721、#4636、#4889(rule-validator)、#4821(dataset-executor)、#4884(driver-sql)均与本文件面不交。


    Generated by Claude Code

  2. claude commented on Aug 3, 2026

    @claude
    Contributor

    ✅ 验收通过 —— PR #4950(draft,等 CI 绿后入队)

    对照实际 diff 复核(3 文件,+209/-9),逐条落实:

    1. 形法对齐,无第三种写法:废弃被扔掉的 timeout(),新 raceCheckTimeout() 与 fix(core): 插件 init/start 超时守卫在 race 落定时被清除 —— 进程不再空转 startupTimeout (#4813) #4874 的 raceStartupTimeout() 同形(try { await Promise.race } finally { clearTimeout(guard) });拒 unref 的理由写进了 TSDoc(unref 的守卫不再钉住事件循环的同时也不再是守卫)。
    2. 测试防的就是错误修法:三条回归里第二条在 fake timers 下数定时器,能识破 unref 式假修复;报告附了红/绿双向验证(换回 main 版本 → 2 条测试红,AssertionError 逐字)。
    3. 范围纪律:严格一文件一修,同形残余按 PD#10 立案 超时守卫「armed 之后扔掉」还剩两处生产实例(另有两处用 unref 代替回收)—— #4813 / #4875 的清仓 #4952(service-automation executeWithTimeout 热路径最值得修)而非顺手扩权;⛔ releases/ 零触碰,changeset 已带。

    验证:@objectstack/core 465 tests 全过;PR CI 已结算项全绿,Test Core / Dogfood / 类型检查在跑。绿后转 ready 入合并队列。


    Generated by Claude Code

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions