From 80960101192ab6f181a5020e35999f5f154d2dee Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Thu, 15 Oct 2026 17:48:00 -0700 Subject: [PATCH] Skip impossible UTC dates in sparse cron searches Parsing a schedule once removes most repeated work, but an annual schedule can still block the event loop during time-zone formatting. Reject impossible UTC minutes and dates before calling Intl. Retain localized hour matching and invalid-date errors. Verified the reproduced formatting-budget regression and focused scheduler, routine, and SQL eligibility tests. The PR preserves the earlier failed timing acceptance and tracks the final live run. Co-Authored-By: Paperclip --- doc/DEVELOPING.md | 2 +- .../services/routines-formatter-cache.test.ts | 18 ++++++++++++++++++ server/src/services/routines.ts | 6 ++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 50593f2c53e..6feb729346c 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -1458,7 +1458,7 @@ the dispatch time of a known future deadline. Failed scans retry at that same interval. Future timers do not hold idle work open, and standby, idle drain and shutdown retain the coordinator's admission gates. External database writers rely on the periodic recovery scan. Sparse cron searches reuse one parsed -expression so repeated parsing cannot block unrelated deadlines. +expression and skip impossible UTC dates before localized formatting. Keep relevant heartbeat extractions and their focused tests in `server/src/services/heartbeat/`. Follow [Heartbeat execution boundaries](../rules/heartbeat-execution-boundaries.mdc) diff --git a/server/src/services/routines-formatter-cache.test.ts b/server/src/services/routines-formatter-cache.test.ts index 93f674d0948..8ff9d4b9ac0 100644 --- a/server/src/services/routines-formatter-cache.test.ts +++ b/server/src/services/routines-formatter-cache.test.ts @@ -20,6 +20,24 @@ describe("nextCronTickInTimeZone formatter caching", () => { expect(parses).toBe(1); }); + it("skips impossible UTC dates before localized formatting", () => { + const format = Intl.DateTimeFormat.prototype.formatToParts; + let formats = 0; + vi.spyOn(Intl.DateTimeFormat.prototype, "formatToParts").mockImplementation(function(this: Intl.DateTimeFormat, date) { + formats++; + if (formats > 48) throw new Error("Sparse UTC schedules must skip impossible dates"); + return format.call(this, date); + }); + const next = nextCronTickInTimeZone("0 12 1 1 *", "UTC", new Date("2020-01-01T12:00:00Z")); + expect(next?.toISOString()).toBe("2021-01-01T12:00:00.000Z"); + expect(formats).toBeLessThanOrEqual(48); + }); + + it.each(["UTC", "America/New_York"])("rejects invalid search dates in %s", timeZone => { + expect(() => nextCronTickInTimeZone("0 12 1 1 *", timeZone, new Date(Number.NaN))) + .toThrow(RangeError); + }); + it("computes the next occurrence of a sparse monthly cron correctly", () => { const next = nextCronTickInTimeZone( "30 6 1 * *", diff --git a/server/src/services/routines.ts b/server/src/services/routines.ts index 642c70f939c..d92bae9560a 100644 --- a/server/src/services/routines.ts +++ b/server/src/services/routines.ts @@ -255,6 +255,12 @@ function getZonedMinuteParts(date: Date, timeZone: string) { } function matchesCronMinute(cron: ReturnType, timeZone: string, date: Date) { + if (timeZone === "UTC" && Number.isFinite(date.getTime()) && ( + !cron.minutes.includes(date.getUTCMinutes()) + || !cron.daysOfMonth.includes(date.getUTCDate()) + || !cron.months.includes(date.getUTCMonth() + 1) + || !cron.daysOfWeek.includes(date.getUTCDay()) + )) return false; const parts = getZonedMinuteParts(date, timeZone); return ( cron.minutes.includes(parts.minute) &&