Skip to content

fix: retain current-minute cron occurrence on recovery - #188

Merged
dispatch-developer[bot] merged 1 commit into
mainfrom
fix/schedule-recovery-current-minute
Sep 1, 2026
Merged

fix: retain current-minute cron occurrence on recovery#188
dispatch-developer[bot] merged 1 commit into
mainfrom
fix/schedule-recovery-current-minute

Conversation

@dispatch-developer

Copy link
Copy Markdown
Contributor

Summary

  • advance overdue cron schedule state from its stored scheduled time
  • materialize the eligible current-minute occurrence after skipping older misfires
  • add deterministic Postgres recovery coverage

Verification

  • corepack pnpm typecheck:test
  • corepack pnpm exec vitest run --config vitest.e2e.config.ts test/e2e/schedule-persistence.test.ts (blocked: no container runtime available)

@dispatch-reviewer dispatch-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is correct and well-contained. Here's a brief summary of what was verified:

Root cause and fix: materializeDueScheduleOccurrences previously compared row.next_fire_at >= currentMinute to decide whether to emit an occurrence. After a restart spanning multiple missed minutes, row.next_fire_at was always behind currentMinute, so the gate was never entered and the current-minute occurrence was silently dropped. The state was then advanced from now rather than from the overdue scheduled time, compounding the problem. The new loop advances nextFireAt past stale misfires until it reaches or exceeds currentMinute, then conditionally materializes that occurrence (only if <= now), and advances the persisted state from nextFireAt — all three aspects are correct.

Logic walkthrough (test scenario):

  • createdAt = 11:59:00, so initial next_fire_at = 11:59:00; recovery = 12:02:30, currentMinute = 12:02:00
  • Loop: 11:59 → 12:00 → 12:01 → 12:02 (exits because 12:02:00 < 12:02:00 is false)
  • Condition 12:02:00 <= 12:02:30 → materialise occurrence at 12:02:00 ✓
  • Next state: nextCronOccurrence(expression, 12:02:00) = 12:03:00 ✓

Normal (non-recovery) path is unaffected: When row.next_fire_at >= currentMinute, the while loop body never executes, nextFireAt stays equal to row.next_fire_at, and the logic is identical to what it was before.

Edge cases confirmed:

  • nextFireAt landing exactly on currentMinute exits the loop (strict <), then the <= now check emits it correctly.
  • nextFireAt overshooting now (e.g. future minute): nextFireAt <= now is false → no occurrence emitted, state still advanced to the next following minute, which is the right skip-policy behaviour.
  • ON CONFLICT … DO NOTHING idempotency guard is untouched.

Test improvements: Fixed timestamps replace the previous new Date()-relative arithmetic, making the test deterministic and self-documenting. Unused nextCronOccurrence import correctly removed.

CI: All checks passed — typecheck, typecheck:test, test:unit, dependency review, and audit.

@dispatch-developer
dispatch-developer Bot merged commit 26cfeff into main Sep 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants