Repository navigation
[Refactor/Chore] Schedule Trigger time model: separate wall-clock times from instants #43937
Description
Self Checks
- I have read the Contributing Guide and Language Policy.
- This is only for refactors or chores; if you would like to ask a question, please head to Discussions.
- I have searched for existing issues search for existing issues, including closed ones.
- I confirm that I am using English to submit this report, otherwise it will be closed.
- 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
- Please do not modify this template :) and fill in all the required fields.
Description
The Schedule Trigger preview can show times that differ from the real runs (month end, DST changes, browser timezone). This issue proposes one time model to fix all of them. Follow-up to #43627.
1. PR plan
- fix(web): keep hourly runs on the right date at month end #43925: month-end fix (already open).
- Frontend time model. No change to execution behavior.
- Backend DST rules plus shared test vectors (one JSON file read by pytest and vitest). The only PR that changes execution behavior.
- Frontend preview follows the same rules.
Tests cover month end, New Year, Feb 29, DST gap and fold (New York, London, Dublin, Santiago, Lord Howe, Chatham), and several browser TZ values.
2. DST rules to confirm
A gap is the hour that does not exist when clocks jump forward. A fold is the hour that happens twice when clocks go back. The gap result depends on the zone and the schedule, so there is no single rule today.
Current backend behavior (calculate_next_run_at on main)
| Zone | Schedule | DST start | DST end |
|---|---|---|---|
| New York | daily 2:30 / 1:30 | moves to 3:00 | runs twice |
| New York | hourly at :30 | 2:30 skipped | both run |
| London | daily 1:30 | moves to 2:00 | runs twice |
| Lord Howe | hourly at :15 | 2:15 moves to 2:30 | |
| Chatham | hourly at :00 | 3:00 skipped |
Proposal, same rule as cronie (man 8 cron)
- Gap, job that runs at a set time or less often than hourly (daily 2:30): run once right after the clocks jump.
- Gap, job that runs hourly or more often (hourly at :30): skip the missing slot.
- Fold, job that runs at a set time or less often than hourly: run once, at the first occurrence.
- Fold, job that runs hourly or more often: run at both occurrences.
These rules change current behavior in two places. In a fold, a daily job runs once instead of twice (see #42959). In a gap, some hourly jobs are skipped instead of moved (Lord Howe). I would like your call here.
Model and Next runs
3. Model
WallTime: year, month, day, hour, minute with no timezone. Date math usesDate.UTConly.Instant: epoch milliseconds.toInstants(wall, timezone)returns 0 (DST gap), 1, or 2 (DST fold) instants.toWall(instant, timezone)usesIntl.- No browser-local
Datemath, so the browser timezone never changes the result.
4. Next runs
- Visual settings become cron, with the same mapping as
ScheduleService.visual_to_cron. Visual and cron share one path. cron-parseronly parses fields. Next runs come from walkingWallTimevalues in the schedule zone, thentoInstantsplus the DST rules.
Motivation
The preview can differ from the real runs.
- Month end: the hourly list skips a month (Schedule Trigger hourly list skips a month at month end #43923).
- DST start: the list shows a time that does not exist, and visual and cron disagree.
- DST end: the backend runs a fixed-time job twice. The list shows the job once.
- Browser timezone: with a DST browser zone, a time can shift by one hour.
The common cause is that the frontend keeps schedule-zone times in browser-local Date objects.
Additional Context
No response
Activity
JHC56 commentedon Oct 12, 2026
cc @Stream29 What do you think about the DST rules in section 2?
I'd lean toward the cronie approach (
man 8 cron), mostly to avoid the double run. A daily job firing twice can mean duplicate emails or duplicate data, and that's hard to undo.That said, keeping the double run has its upsides. Existing schedules wouldn't change, and the backend stays as is. Jobs that are safe to repeat aren't affected either. Happy to go either way.
Stream29 commentedon Oct 12, 2026
Thanks for the thoughtful write-up and careful investigation!
JHC56 commentedon Oct 12, 2026
Thanks! #43925 stands on its own and can be merged anytime. I'll start on step 2 next, since that step doesn't change execution behavior.
Step 3 does change execution behavior, so I'll check with you before starting it.
I'll put the details in each PR instead of opening separate issues. Happy to open them too if you prefer.
JHC56 commentedon Oct 12, 2026
@Stream29 I’m working on the step 2 patch now. I’d like to send steps 1 to 4 as a series of small PRs, each one building on the last. Since I’m working from a fork, I’ll open each step against main after the previous one is merged, starting with step 1.
A later step may change or remove some code from an earlier step, but the overall goal stays the same. Could you take a look at #43925 when you get a chance?
JHC56 commentedon Oct 14, 2026
cc @Stream29 @crazywoola I opened #44065 for step 2 of this refactor.
The problems are already tracked here, so I described them in the PR instead of opening a new issue.
FYI. this step only changes the frontend, and execution behavior stays the same. Could you take a look when you have time?
JHC56 commentedon Oct 16, 2026
After step 2 (#44065) is reviewed and merged, I plan to start step 3. Step 3 changes when jobs run, so I'd like to agree on these first.
Related, #43532 fixes another preview time mismatch. The PR was approved, but the bot dropped it from the merge queue because of an unrelated test failure.
Could you add the PR back to the queue? Merging the PR before step 3 would help, so new nodes get the DST rules too.
JHC56 commentedon Oct 19, 2026
Thanks for merging #43532.
I updated #44065 to match the current main. I fixed the conflicts and kept the new tests from #44260. I only changed
wallTimeto read times in the schedule timezone. All checks pass.