mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 04:01:23 +00:00
fix: reset stale execution state after SIGUSR1 in-process restart (#15195)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 676f9ec451
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:
@@ -173,6 +173,59 @@ describe("heartbeat-wake", () => {
|
||||
expect(handler).toHaveBeenCalledWith({ reason: "exec-event" });
|
||||
});
|
||||
|
||||
it("resets running/scheduled flags when new handler is registered", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Simulate a handler that's mid-execution when SIGUSR1 fires.
|
||||
// We do this by having the handler hang forever (never resolve).
|
||||
let resolveHang: () => void;
|
||||
const hangPromise = new Promise<void>((r) => {
|
||||
resolveHang = r;
|
||||
});
|
||||
const handlerA = vi
|
||||
.fn()
|
||||
.mockReturnValue(hangPromise.then(() => ({ status: "ran" as const, durationMs: 1 })));
|
||||
setHeartbeatWakeHandler(handlerA);
|
||||
|
||||
// Trigger the handler — it starts running but never finishes
|
||||
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handlerA).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Now simulate SIGUSR1: register a new handler while handlerA is still running.
|
||||
// Without the fix, `running` would stay true and handlerB would never fire.
|
||||
const handlerB = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||
setHeartbeatWakeHandler(handlerB);
|
||||
|
||||
// handlerB should be able to fire (running was reset)
|
||||
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handlerB).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clean up the hanging promise
|
||||
resolveHang!();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
it("clears stale retry cooldown when a new handler is registered", async () => {
|
||||
vi.useFakeTimers();
|
||||
const handlerA = vi.fn().mockResolvedValue({ status: "skipped", reason: "requests-in-flight" });
|
||||
setHeartbeatWakeHandler(handlerA);
|
||||
|
||||
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handlerA).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Simulate SIGUSR1 startup with a fresh wake handler.
|
||||
const handlerB = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||
setHeartbeatWakeHandler(handlerB);
|
||||
|
||||
requestHeartbeatNow({ reason: "manual", coalesceMs: 0 });
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handlerB).toHaveBeenCalledTimes(1);
|
||||
expect(handlerB).toHaveBeenCalledWith({ reason: "manual" });
|
||||
});
|
||||
|
||||
it("drains pending wake once a handler is registered", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
|
||||
@@ -146,6 +146,23 @@ export function setHeartbeatWakeHandler(next: HeartbeatWakeHandler | null): () =
|
||||
handlerGeneration += 1;
|
||||
const generation = handlerGeneration;
|
||||
handler = next;
|
||||
if (next) {
|
||||
// New lifecycle starting (e.g. after SIGUSR1 in-process restart).
|
||||
// Clear any timer metadata from the previous lifecycle so stale retry
|
||||
// cooldowns do not delay a fresh handler.
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = null;
|
||||
timerDueAt = null;
|
||||
timerKind = null;
|
||||
// Reset module-level execution state that may be stale from interrupted
|
||||
// runs in the previous lifecycle. Without this, `running === true` from
|
||||
// an interrupted heartbeat blocks all future schedule() attempts, and
|
||||
// `scheduled === true` can cause spurious immediate re-runs.
|
||||
running = false;
|
||||
scheduled = false;
|
||||
}
|
||||
if (handler && pendingWake) {
|
||||
schedule(DEFAULT_COALESCE_MS, "normal");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user