mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 15:35:12 +01:00
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 <noreply@paperclip.ing>
This commit is contained in:
1 parent
da062a3971
commit
8096010119
3 files changed
+25
-1
No files matched your search
+1
-1
@@ -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)
|
||||
|
||||
@@ -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 * *",
|
||||
|
||||
@@ -255,6 +255,12 @@ function getZonedMinuteParts(date: Date, timeZone: string) {
|
||||
}
|
||||
|
||||
function matchesCronMinute(cron: ReturnType<typeof parseCron>, 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) &&
|
||||
|
||||
Reference in new issue
Block a user