Skip to content

cron: fix day-of-week off-by-one in crontab schedules#138

Merged
pufit merged 1 commit into
ClickHouse:mainfrom
alex-fedotyev:alex/cron-dow-fix
Jun 23, 2026
Merged

cron: fix day-of-week off-by-one in crontab schedules#138
pufit merged 1 commit into
ClickHouse:mainfrom
alex-fedotyev:alex/cron-dow-fix

Conversation

@alex-fedotyev

Copy link
Copy Markdown
Contributor

Summary

APScheduler's numeric day_of_week is 0=Mon..6=Sun, but Unix crontab uses 0=Sun..6=Sat (and 7 also means Sunday). CronTrigger.from_crontab() passes the number straight through without remapping, so every cron whose schedule uses a numeric day-of-week fires one weekday late. The shipped weekly skill-reviser job (0 3 * * 0, intended Sunday) actually fires Monday, and 0 13 * * 1 (Monday) fires Tuesday.

This adds _crontab_to_trigger(), a drop-in replacement for from_crontab() that translates the day-of-week field to APScheduler's day-name aliases (mon, tue, and so on). It preserves *, ranges (1-5), lists (1,4), and step suffixes (*/2), and leaves the other four fields and the timezone behaviour exactly as before. The three places that build a trigger from a crontab string now route through it:

  • job scheduling (_make_trigger)
  • source ingestion scheduling
  • the startup overdue check (_is_overdue)

Only the day-of-week interpretation changes. A schedule without a numeric day-of-week field produces the same trigger as before; there is a test asserting parity with from_crontab.

Rollout note

Schedules are read at startup, so this takes effect on the next daemon restart. After the restart, every numeric day-of-week cron shifts to the weekday its schedule already specifies. That is the intended correction, not a change to the schedule strings.

A follow-up will harden startup catch-up so a weekly job that has never recorded a successful run can still be backfilled after a missed fire.

Test plan

  • pytest tests/test_cron.py: 82 passed locally.
  • New TestCrontabToTrigger covers numeric Monday/Sunday/7, range 1-5 (Mon to Fri), list 1,4 (Mon and Thu), * (every day), day-name passthrough, parity with from_crontab for non-DOW schedules, and the interval-string fallback still raising ValueError.
  • New _is_overdue case proves a weekly Monday job is overdue after exactly one week, not 6 or 8 days.
  • Verified in-container that from_crontab("0 13 * * 1") returns a Tuesday while _crontab_to_trigger("0 13 * * 1") returns a Monday.

APScheduler's numeric day_of_week is 0=Mon..6=Sun, while Unix crontab
is 0=Sun..6=Sat (7 also means Sun). CronTrigger.from_crontab passes the
number straight through without remapping, so every numeric day-of-week
schedule fired one weekday late: "0 13 * * 1" (Monday) actually ran on
Tuesday, and "0 3 * * 0" (Sunday) ran on Monday.

Add _crontab_to_trigger, a from_crontab replacement that translates the
day-of-week field to APScheduler's day-name aliases (preserving *,
ranges like 1-5, lists like 1,4, and step suffixes) and leaves the
other four fields and the timezone handling unchanged. Route the three
trigger-construction sites (job scheduling, source scheduling, and the
overdue check) through it.

Tests cover the day-of-week mapping, parity with from_crontab for
non-DOW schedules, the interval-string fallback, and a weekly
_is_overdue case that is due after exactly one week.

After a restart, every numeric day-of-week cron shifts to the day its
schedule already specifies; that is the intended correction.
@pufit pufit merged commit 663cee0 into ClickHouse:main Jun 23, 2026
pufit added a commit to fallintoplace/nerve that referenced this pull request Jun 23, 2026
Resolve conflicts with ClickHouse#138 (_crontab_to_trigger): thread the configured
timezone through the helper at all call sites instead of reverting to
host-tz CronTrigger.from_crontab. Also set config.timezone in the
wakeup-sweep test fixture so CronService.__init__ accepts it.
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.

2 participants