fix: prevent heartbeat scheduler silent death from wake handler race (#15108)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: fd7165b935
Co-authored-by: joeykrug <5925937+joeykrug@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Joseph Krug
2026-02-12 23:30:21 -04:00
committed by GitHub
parent ec44e262be
commit 40aff672c1
5 changed files with 282 additions and 22 deletions

View File

@@ -880,6 +880,7 @@ export function startHeartbeatRunner(opts: {
}
const delay = Math.max(0, nextDue - now);
state.timer = setTimeout(() => {
state.timer = null;
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
}, delay);
state.timer.unref?.();
@@ -933,6 +934,12 @@ export function startHeartbeatRunner(opts: {
};
const run: HeartbeatWakeHandler = async (params) => {
if (state.stopped) {
return {
status: "skipped",
reason: "disabled",
} satisfies HeartbeatRunResult;
}
if (!heartbeatsEnabled) {
return {
status: "skipped",
@@ -994,12 +1001,16 @@ export function startHeartbeatRunner(opts: {
return { status: "skipped", reason: isInterval ? "not-due" : "disabled" };
};
setHeartbeatWakeHandler(async (params) => run({ reason: params.reason }));
const wakeHandler: HeartbeatWakeHandler = async (params) => run({ reason: params.reason });
const disposeWakeHandler = setHeartbeatWakeHandler(wakeHandler);
updateConfig(state.cfg);
const cleanup = () => {
if (state.stopped) {
return;
}
state.stopped = true;
setHeartbeatWakeHandler(null);
disposeWakeHandler();
if (state.timer) {
clearTimeout(state.timer);
}