mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 11:24:58 +00:00
fix(cron): re-arm timer when onTimer fires during active job execution (#14233)
* fix(cron): re-arm timer when onTimer fires during active job execution When a cron job takes longer than MAX_TIMER_DELAY_MS (60s), the clamped timer fires while state.running is still true. The early return in onTimer() previously exited without re-arming the timer, leaving no setTimeout scheduled. This silently kills the cron scheduler until the next gateway restart. The fix calls armTimer(state) before the early return so the scheduler continues ticking even when a job is in progress. This is the likely root cause of recurring cron jobs silently skipping, as reported in #12025. One-shot (kind: 'at') jobs were unaffected because they typically complete within a single timer cycle. Includes a regression test that simulates a slow job exceeding the timer clamp period and verifies the next occurrence still fires. * fix: update tests for timer re-arm behavior - Update existing regression test to expect timer re-arm with non-zero delay instead of no timer at all - Simplify new test to directly verify state.timer is set after onTimer returns early due to running guard * fix: use fixed 60s delay for re-arm to prevent zero-delay hot-loop When the running guard re-arms the timer, use MAX_TIMER_DELAY_MS directly instead of calling armTimer() which can compute a zero delay for past-due jobs. This prevents a tight spin while still keeping the scheduler alive. * style: add curly braces to satisfy eslint(curly) rule
This commit is contained in:
@@ -212,7 +212,7 @@ describe("Cron issue regressions", () => {
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("does not hot-loop zero-delay timers while a run is already in progress", async () => {
|
||||
it("re-arms timer without hot-looping when a run is already in progress", async () => {
|
||||
const timeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
||||
const store = await makeStorePath();
|
||||
const now = Date.parse("2026-02-06T10:05:00.000Z");
|
||||
@@ -233,8 +233,15 @@ describe("Cron issue regressions", () => {
|
||||
|
||||
await onTimer(state);
|
||||
|
||||
expect(timeoutSpy).not.toHaveBeenCalled();
|
||||
expect(state.timer).toBeNull();
|
||||
// The timer should be re-armed (not null) so the scheduler stays alive,
|
||||
// with a fixed MAX_TIMER_DELAY_MS (60s) delay to avoid a hot-loop when
|
||||
// past-due jobs are waiting. See #12025.
|
||||
expect(timeoutSpy).toHaveBeenCalled();
|
||||
expect(state.timer).not.toBeNull();
|
||||
const delays = timeoutSpy.mock.calls
|
||||
.map(([, delay]) => delay)
|
||||
.filter((d): d is number => typeof d === "number");
|
||||
expect(delays).toContain(60_000);
|
||||
timeoutSpy.mockRestore();
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user