fix(cron): prevent duplicate fires when multiple jobs trigger simultaneously (#14256)

The `computeNextRunAtMs` function used `nowSecondMs - 1` as the
reference time for croner's `nextRun()`, which caused it to return the
current second as a valid next-run time. When a job fired at e.g.
11:00:00.500, computing the next run still yielded 11:00:00.000 (same
second, already elapsed), causing the scheduler to immediately re-fire
the job in a tight loop (15-21x observed in the wild).

Fix: use `nowSecondMs` directly (no `-1` lookback) and change the
return guard from `>=` to `>` so next-run is always strictly after
the current second.

Fixes #14164
This commit is contained in:
Xinhua Gu
2026-02-12 05:04:17 +01:00
committed by GitHub
parent b912d3992d
commit dd6047d998
2 changed files with 18 additions and 16 deletions

View File

@@ -39,22 +39,22 @@ describe("cron schedule", () => {
const dailyNoon = { kind: "cron" as const, expr: "0 0 12 * * *", tz: "UTC" };
const noonMs = Date.parse("2026-02-08T12:00:00.000Z");
it("returns current occurrence when nowMs is exactly at the match", () => {
it("advances past current second when nowMs is exactly at the match", () => {
// Fix #14164: must NOT return the current second — that caused infinite
// re-fires when multiple jobs triggered simultaneously.
const next = computeNextRunAtMs(dailyNoon, noonMs);
expect(next).toBe(noonMs);
expect(next).toBe(noonMs + 86_400_000); // next day
});
it("returns current occurrence when nowMs is mid-second (.500) within the match", () => {
// This is the core regression: without the second-floor fix, a 1ms
// lookback from 12:00:00.499 still lands inside the matching second,
// causing croner to skip to the *next day*.
it("advances past current second when nowMs is mid-second (.500) within the match", () => {
// Fix #14164: returning the current second caused rapid duplicate fires.
const next = computeNextRunAtMs(dailyNoon, noonMs + 500);
expect(next).toBe(noonMs);
expect(next).toBe(noonMs + 86_400_000); // next day
});
it("returns current occurrence when nowMs is late in the matching second (.999)", () => {
it("advances past current second when nowMs is late in the matching second (.999)", () => {
const next = computeNextRunAtMs(dailyNoon, noonMs + 999);
expect(next).toBe(noonMs);
expect(next).toBe(noonMs + 86_400_000); // next day
});
it("advances to next day once the matching second is fully past", () => {