Repository navigation
Implement full 5-field cron matching in scheduled exports and isolate one schedule's failure from the rest #1509
Description
Summary
isValidCronExpression accepts steps (*/15), lists (0,30) and ranges (1-5), but computeNextRunAt only understands * or a single number for minute and hour. It ignores day-of-month, month and day-of-week entirely. A schedule */15 * * * * never matches: the loop spins 532,800 iterations, then falls back to "next minute". 0 9 * * 1 (Mondays) runs every day. In runDueSchedules, one schedule that throws stops the loop before its nextRunAt is advanced, so it fails first on every tick and blocks all later schedules. This issue implements correct cron matching and per-schedule failure isolation.
Background and evidence
In src/services/scheduledExports.ts:
- Validation is broader than matching.
isValidCronExpression(about lines 88-92) validates^([*]|\d+|\*\/\d+|\d+-\d+)(,(...))*$per field. - Matching handles almost nothing.
computeNextRunAt(about lines 94-112) destructures only[minuteField, hourField]and comparesnext.getUTCMinutes() === Number(minuteField).Number('*/15')isNaN, so it never matches. The loop is bounded at60 * 24 * 370iterations, thenreturn new Date(from.getTime() + 60_000). - One failure blocks the queue.
runDueSchedules(about lines 273-289) doesresults.push(await this.runSchedule(schedule, now, signal)); await this.scheduleStore.update(schedule.id, { lastRunAt: now, nextRunAt: computeNextRunAt(...) }). IfrunSchedulethrows, the update never happens and the loop exits. - Routes.
src/routes/exports/schedules.tsaccepts anycronstring up to 100 characters (line 21) and relies onisValidCronExpression.
Scope
In scope
- Cron engine.
- Implement a small, dependency-free 5-field matcher in
src/lib/cron.ts: minute 0-59, hour 0-23, day-of-month 1-31, month 1-12, day-of-week 0-6 with 7 as Sunday. Support*, numbers,a-b,*/n,a-b/nand comma lists. - Apply standard POSIX semantics for day-of-month OR day-of-week when both are restricted.
- All in UTC.
- Implement a small, dependency-free 5-field matcher in
- Efficient next-run. Compute
nextRunby advancing field by field (skip to the next matching month, then day, hour, minute), not minute by minute. It returnsnullfor impossible expressions such as0 0 31 2 *, and validation rejects those. - Validation parity.
isValidCronExpressionuses the same parser, so anything that validates can be scheduled. Out-of-range values (61 * * * *) are rejected with a field-specific error in the route. - Failure isolation.
runDueSchedulescatches per-schedule errors, recordslastError/lastErrorAt, always advancesnextRunAt, and continues with the remaining schedules. It returns per-schedule outcomes.- Aborts (
signal.aborted) still stop the loop.
- Docs. Update
docs/scheduled-exports.mdwith the supported syntax.
Out of scope
- Time zones other than UTC.
- Wiring the worker at boot.
Design constraints
- Fast.
nextRunis O(fields × small constant), and benchmarked under 1 ms for any valid expression. - Pure. The parser is pure and fully unit-testable; no new runtime dependency.
- Shutdown. Abort behaviour stays cooperative.
Test plan
Add src/lib/cron.test.ts (table-driven, plus fast-check) and extend src/services/scheduledExports.test.ts.
| # | Scenario | Expected |
|---|---|---|
| 1 | */15 * * * * from 10:07 |
10:15 |
| 2 | 0 9 * * 1 from Tuesday |
next Monday 09:00 UTC |
| 3 | 0,30 8-10 * * * |
08:00, 08:30 ... 10:30 sequence |
| 4 | 0 0 1 * * from Jan 31 |
Feb 1 00:00 |
| 5 | 0 0 29 2 * |
next leap-year Feb 29 |
| 6 | 0 0 31 2 * |
validation rejects |
| 7 | 61 * * * * |
validation rejects with a field error |
| 8 | Property | nextRun(e, t) > t and matches e for random valid expressions |
| 9 | First of three schedules throws | the other two run; the failing one's nextRunAt advanced and lastError set |
| 10 | Abort mid-loop | loop stops; abort surfaced |
Regression proof. Row 1 returns from + 1 minute (and row 2 returns tomorrow) on current main.
Acceptance criteria
- Every test-plan row passes.
-
npm run lint,npm run typecheck,npm run buildandnpm testpass, and CI is green. - At least 95% line coverage on
src/lib/cron.tsand 90% on the touchedscheduledExports.tscode.
Getting started
git checkout -b fix/scheduled-exports-cron
npm ci && npx prisma generate
npm run test:unit -- src/services/scheduledExportsRead first: isValidCronExpression, computeNextRunAt and runDueSchedules in src/services/scheduledExports.ts, and src/routes/exports/schedules.ts.
PR requirements
- Link this issue (
Closes #<n>). - Include the benchmark numbers for
nextRun. - Paste test output and coverage.
- Example commit:
fix(exports): implement full cron matching and isolate schedule failures.
Priority rationale
Medium. It is a self-contained module, but schedules currently run at the wrong times, and one bad schedule silently starves all the others.
Activity
D0L4P0 commentedon Oct 18, 2026
@D0L4P0 has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @D0L4P0 to this issue.
drips-wave commentedon Oct 19, 2026
Congratulations, @D0L4P0! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.
🧑💻 @D0L4P0: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.
Warning
When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.
🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.
🌊 Happy Wave 🌊