mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 11:41:24 +00:00
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:
@@ -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", () => {
|
||||
|
||||
@@ -50,16 +50,18 @@ export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): numbe
|
||||
catch: false,
|
||||
});
|
||||
// Cron operates at second granularity, so floor nowMs to the start of the
|
||||
// current second. This prevents the lookback from landing inside a matching
|
||||
// second — if nowMs is e.g. 12:00:00.500 and the pattern fires at second 0,
|
||||
// a 1ms lookback (12:00:00.499) is still *within* that second, causing
|
||||
// croner to skip ahead to the next occurrence (e.g. the following day).
|
||||
// Flooring first ensures the lookback always falls in the *previous* second.
|
||||
// current second. We ask croner for the next occurrence strictly *after*
|
||||
// nowSecondMs so that a job whose schedule matches the current second is
|
||||
// never re-scheduled into the same (already-elapsed) second.
|
||||
//
|
||||
// Previous code used `nowSecondMs - 1` which caused croner to return the
|
||||
// current second as a valid next-run, leading to rapid duplicate fires when
|
||||
// multiple jobs triggered simultaneously (see #14164).
|
||||
const nowSecondMs = Math.floor(nowMs / 1000) * 1000;
|
||||
const next = cron.nextRun(new Date(nowSecondMs - 1));
|
||||
const next = cron.nextRun(new Date(nowSecondMs));
|
||||
if (!next) {
|
||||
return undefined;
|
||||
}
|
||||
const nextMs = next.getTime();
|
||||
return Number.isFinite(nextMs) && nextMs >= nowSecondMs ? nextMs : undefined;
|
||||
return Number.isFinite(nextMs) && nextMs > nowSecondMs ? nextMs : undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user