Skip to content

[Refactor/Chore] Schedule Trigger time model: separate wall-clock times from instants #43937

Open

Description

@JHC56

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

  1. fix(web): keep hourly runs on the right date at month end #43925: month-end fix (already open).
  2. Frontend time model. No change to execution behavior.
  3. Backend DST rules plus shared test vectors (one JSON file read by pytest and vitest). The only PR that changes execution behavior.
  4. 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 uses Date.UTC only.
  • Instant: epoch milliseconds.
  • toInstants(wall, timezone) returns 0 (DST gap), 1, or 2 (DST fold) instants. toWall(instant, timezone) uses Intl.
  • No browser-local Date math, so the browser timezone never changes the result.

4. Next runs

  1. Visual settings become cron, with the same mapping as ScheduleService.visual_to_cron. Visual and cron share one path.
  2. cron-parser only parses fields. Next runs come from walking WallTime values in the schedule zone, then toInstants plus the DST rules.

Motivation

The preview can differ from the real runs.

  1. Month end: the hourly list skips a month (Schedule Trigger hourly list skips a month at month end #43923).
  2. DST start: the list shows a time that does not exist, and visual and cron disagree.
  3. DST end: the backend runs a fixed-time job twice. The list shows the job once.
  4. 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

  1. JHC56 commented on Oct 12, 2026

    @JHC56
    ContributorAuthor

    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.

  2. Stream29 commented on Oct 12, 2026

    @Stream29
    Contributor

    Thanks for the thoughtful write-up and careful investigation!

  3. JHC56 commented on Oct 12, 2026

    @JHC56
    ContributorAuthor

    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.

  4. JHC56 commented on Oct 12, 2026

    @JHC56
    ContributorAuthor

    @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?

  5. JHC56 commented on Oct 14, 2026

    @JHC56
    ContributorAuthor

    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?

  6. JHC56 commented on Oct 16, 2026

    @JHC56
    ContributorAuthor

    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.

    1. Should a daily job run once in a repeated hour, or twice like today?
    2. Should an hourly job always skip a missing slot? Today the result depends on the zone.
    3. Should a job count as hourly when it runs every hour or more often, like cron (man 8 cron)?
    4. Should the new DST rules in the issue description apply to existing schedules right away?

    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.

  7. JHC56 commented on Oct 19, 2026

    @JHC56
    ContributorAuthor

    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 wallTime to read times in the schedule timezone. All checks pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    review: mediumghfind author score; see https://ghfind.com

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions