Skip to content

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

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)。

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions