From 7154bc6857ec16f1db59e378d16af9e698cd52d3 Mon Sep 17 00:00:00 2001 From: James Groat Date: Thu, 1 Jan 2026 17:27:43 -0700 Subject: [PATCH] fix(cron): prevent every schedule from firing in infinite loop When anchorMs is not provided (always in production), the schedule computed nextRunAtMs as nowMs, causing jobs to fire immediately and repeatedly instead of at the configured interval. - Change nowMs <= anchor to nowMs < anchor to prevent early return - Add Math.max(1, ...) to ensure steps is always at least 1 - Add test for anchorMs not provided case --- src/cron/schedule.test.ts | 8 ++++++++ src/cron/schedule.ts | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cron/schedule.test.ts b/src/cron/schedule.test.ts index 210e2860a..f38bc6a2d 100644 --- a/src/cron/schedule.test.ts +++ b/src/cron/schedule.test.ts @@ -23,4 +23,12 @@ describe("cron schedule", () => { ); expect(next).toBe(anchor + 30_000); }); + + it("computes next run for every schedule when anchorMs is not provided", () => { + const now = Date.parse("2025-12-13T00:00:00.000Z"); + const next = computeNextRunAtMs({ kind: "every", everyMs: 30_000 }, now); + + // Should return nowMs + everyMs, not nowMs (which would cause infinite loop) + expect(next).toBe(now + 30_000); + }); }); diff --git a/src/cron/schedule.ts b/src/cron/schedule.ts index 4c4308da8..bdf92ea13 100644 --- a/src/cron/schedule.ts +++ b/src/cron/schedule.ts @@ -12,9 +12,9 @@ export function computeNextRunAtMs( if (schedule.kind === "every") { const everyMs = Math.max(1, Math.floor(schedule.everyMs)); const anchor = Math.max(0, Math.floor(schedule.anchorMs ?? nowMs)); - if (nowMs <= anchor) return anchor; + if (nowMs < anchor) return anchor; const elapsed = nowMs - anchor; - const steps = Math.floor((elapsed + everyMs - 1) / everyMs); + const steps = Math.max(1, Math.floor((elapsed + everyMs - 1) / everyMs)); return anchor + steps * everyMs; }