发现自 #4875 的实现过程(PR #4950)。未认领。 与 #4813、#4875 是同一种形状的剩余实例。
#4813(PR #4874,kernel.ts 的 init/start)和 #4875(PR #4950,health-monitor.ts 的健康检查)
各修掉一处。修 #4875 时顺手扫了仓里所有 Promise.race + setTimeout 的守卫点,剩下这些:
A. 真·漏(守卫 armed 后既不 clear 也不 unref)
A1. packages/services/service-automation/src/engine.ts:4296 executeWithTimeout()
private executeWithTimeout(
promise: Promise< NodeExecutionResult >,
timeoutMs: number,
nodeId: string,
): Promise< NodeExecutionResult > {
return Promise.race([
promise,
new Promise< NodeExecutionResult >((_, reject) =>
setTimeout(() => reject(new Error(`Node '${nodeId}' timed out after ${timeoutMs}ms`)), timeoutMs),
),
]);
}
调用点在 :3921,每个流程节点一根。这是三处里最值得修的:节点执行是热路径,漏的量级随
流程复杂度和触发频率线性增长,而且和 #4875 一样会把一次性进程(os CLI 里跑到 flow 的路径)
按 timeoutMs 钉住。
A2. packages/core/src/hot-reload.ts:275 —— 插件 destroy() 的 shutdown 守卫:
const shutdownPromise = plugin.destroy();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Shutdown timeout')), config.shutdownTimeout);
});
await Promise.race([shutdownPromise, timeoutPromise]);
与 #4813 修掉的两处一字不差。热重载路径今天调用不多,但漏法相同。
B. 用 unref() 代替回收(需要一次判定,不一定是 bug)
B1. packages/core/src/kernel.ts:430 shutdown() —— PR #4874 的 changeset 已点名这处
用的是 unref(),并写明为什么 unref() 在 init/start 那两处是错的:unref'd 的守卫不再钉住
事件循环的同时也不再是一个守卫 —— 若 hook 永不 settle 且没有别的东西撑着事件循环,Node
会在定时器触发前退出,超时被静默吞掉。shutdown 是不是同一回事需要判一次:那里的语义可能是
「进程反正要退了,吞掉也无所谓」,也可能同样应该改成 finally { clearTimeout }。
B2. packages/objectql/src/engine.ts:2971 checkDriversHealth() —— 显式 unref(),
注释写着「Never hold the event loop open for a probe」。同样是 B1 的判定题:一个被静默吞掉的
探针超时会让 checkHealth 挂死时永远不返回,而不是报超时。
已经是对的(留档,免得下次重扫)
packages/objectql/src/hook-wrappers.ts:236 —— finally { clearTimeout } ✅
packages/runtime/src/app-plugin.ts:1159 —— race 后 clearTimeout(timer) ✅
packages/services/service-job/src/run-with-policy.ts:50 —— .finally(() => clearTimeout(timer)) ✅
packages/plugins/driver-mongodb/src/test-mongod.ts:72 —— finally { clearTimeout } ✅
建议
A1/A2 直接按 #4874 的同形 helper 收编(try { await Promise.race(...) } finally { clearTimeout(guard) }),
回归测试照 PR #4950 的三条写:赢下 race 不留 ref'd 定时器 / 多轮不累积(fake timers 计数,能识破
unref() 式假修复)/ 真挂住时超时照常上报。B1/B2 需要维护者先判语义,再决定是否改。
拆成一个 issue 而不是四个,是因为四处共用同一条判据和同一个修法;如果 B 组的判定结论是「保持
unref()」,建议在那两处补一行注释指向 #4813 的理由,免得下一个扫仓的人再提一次。
发现自 #4875 的实现过程(PR #4950)。未认领。 与 #4813、#4875 是同一种形状的剩余实例。
#4813(PR #4874,
kernel.ts的 init/start)和 #4875(PR #4950,health-monitor.ts的健康检查)各修掉一处。修 #4875 时顺手扫了仓里所有
Promise.race+setTimeout的守卫点,剩下这些:A. 真·漏(守卫 armed 后既不 clear 也不 unref)
A1.
packages/services/service-automation/src/engine.ts:4296executeWithTimeout()调用点在
:3921,每个流程节点一根。这是三处里最值得修的:节点执行是热路径,漏的量级随流程复杂度和触发频率线性增长,而且和 #4875 一样会把一次性进程(
osCLI 里跑到 flow 的路径)按
timeoutMs钉住。A2.
packages/core/src/hot-reload.ts:275—— 插件destroy()的 shutdown 守卫:与 #4813 修掉的两处一字不差。热重载路径今天调用不多,但漏法相同。
B. 用
unref()代替回收(需要一次判定,不一定是 bug)B1.
packages/core/src/kernel.ts:430shutdown()—— PR #4874 的 changeset 已点名这处用的是
unref(),并写明为什么unref()在 init/start 那两处是错的:unref'd 的守卫不再钉住事件循环的同时也不再是一个守卫 —— 若 hook 永不 settle 且没有别的东西撑着事件循环,Node
会在定时器触发前退出,超时被静默吞掉。shutdown 是不是同一回事需要判一次:那里的语义可能是
「进程反正要退了,吞掉也无所谓」,也可能同样应该改成
finally { clearTimeout }。B2.
packages/objectql/src/engine.ts:2971checkDriversHealth()—— 显式unref(),注释写着「Never hold the event loop open for a probe」。同样是 B1 的判定题:一个被静默吞掉的
探针超时会让
checkHealth挂死时永远不返回,而不是报超时。已经是对的(留档,免得下次重扫)
packages/objectql/src/hook-wrappers.ts:236——finally { clearTimeout }✅packages/runtime/src/app-plugin.ts:1159—— race 后clearTimeout(timer)✅packages/services/service-job/src/run-with-policy.ts:50——.finally(() => clearTimeout(timer))✅packages/plugins/driver-mongodb/src/test-mongod.ts:72——finally { clearTimeout }✅建议
A1/A2 直接按 #4874 的同形 helper 收编(
try { await Promise.race(...) } finally { clearTimeout(guard) }),回归测试照 PR #4950 的三条写:赢下 race 不留 ref'd 定时器 / 多轮不累积(fake timers 计数,能识破
unref()式假修复)/ 真挂住时超时照常上报。B1/B2 需要维护者先判语义,再决定是否改。拆成一个 issue 而不是四个,是因为四处共用同一条判据和同一个修法;如果 B 组的判定结论是「保持
unref()」,建议在那两处补一行注释指向 #4813 的理由,免得下一个扫仓的人再提一次。