Skip to content

Commit b0befb5

Browse files
fix(cron): handle legacy atMs field in schedule when computing next run (#9932)
* fix(cron): handle legacy atMs field in schedule when computing next run The cron scheduler only checked for `schedule.at` (string) but legacy jobs may have `schedule.atMs` (number) from before the schema migration. This caused nextRunAtMs to stay null because: 1. Store migration runs on load but may not persist immediately 2. Race conditions or file mtime issues can skip migration 3. computeJobNextRunAtMs/computeNextRunAtMs only checked `at`, not `atMs` Fix: Make both functions defensive by checking `atMs` first (number), then `atMs` (string, for edge cases), then falling back to `at` (string). This ensures jobs fire correctly even if: - Migration hasn't run yet - Old data was written by a previous version - The store was manually edited Fixes #9930 * fix: validate numeric atMs to prevent NaN/Infinity propagation Addresses review feedback - numeric atMs values are now validated with Number.isFinite() && atMs > 0 before use. This prevents corrupted or manually edited stores from causing hot timer loops via setTimeout(..., NaN).
1 parent 40e23b0 commit b0befb5

File tree

2 files changed

+24
-2
lines changed

2 files changed

+24
-2
lines changed

src/cron/schedule.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@ import { parseAbsoluteTimeMs } from "./parse.js";
44

55
export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): number | undefined {
66
if (schedule.kind === "at") {
7-
const atMs = parseAbsoluteTimeMs(schedule.at);
7+
// Handle both canonical `at` (string) and legacy `atMs` (number) fields.
8+
// The store migration should convert atMs→at, but be defensive in case
9+
// the migration hasn't run yet or was bypassed.
10+
const sched = schedule as { at?: string; atMs?: number | string };
11+
const atMs =
12+
typeof sched.atMs === "number" && Number.isFinite(sched.atMs) && sched.atMs > 0
13+
? sched.atMs
14+
: typeof sched.atMs === "string"
15+
? parseAbsoluteTimeMs(sched.atMs)
16+
: typeof sched.at === "string"
17+
? parseAbsoluteTimeMs(sched.at)
18+
: null;
819
if (atMs === null) {
920
return undefined;
1021
}

src/cron/service/jobs.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,18 @@ export function computeJobNextRunAtMs(job: CronJob, nowMs: number): number | und
5252
if (job.state.lastStatus === "ok" && job.state.lastRunAtMs) {
5353
return undefined;
5454
}
55-
const atMs = parseAbsoluteTimeMs(job.schedule.at);
55+
// Handle both canonical `at` (string) and legacy `atMs` (number) fields.
56+
// The store migration should convert atMs→at, but be defensive in case
57+
// the migration hasn't run yet or was bypassed.
58+
const schedule = job.schedule as { at?: string; atMs?: number | string };
59+
const atMs =
60+
typeof schedule.atMs === "number" && Number.isFinite(schedule.atMs) && schedule.atMs > 0
61+
? schedule.atMs
62+
: typeof schedule.atMs === "string"
63+
? parseAbsoluteTimeMs(schedule.atMs)
64+
: typeof schedule.at === "string"
65+
? parseAbsoluteTimeMs(schedule.at)
66+
: null;
5667
return atMs !== null ? atMs : undefined;
5768
}
5869
return computeNextRunAtMs(job.schedule, nowMs);

0 commit comments

Comments
 (0)