Skip to content

fix(schedule): never schedule a cron run in the past or skip a DST day - #174

Merged
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/cron-dst-next-run
Oct 22, 2026
Merged

M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/cron-dst-next-run

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Summary

What was wrong. ScheduleTimeZone::next_after returned the first match from cron::Schedule::after(..). With cron 0.17 (the latest release), that has two DST bugs:

  • Fall-back: in the repeated hour, cron yields the earlier instant of an ambiguous wall-clock time first, even when it is before from. With */15 * * * * in America/New_York and from = 2026-11-01T06:10Z (01:10 EST, second pass), the next run came back as 05:15Z, 55 minutes in the past.
  • Spring-forward: a wall-clock time inside the gap does not exist, and cron silently skips it. A daily 30 2 * * * never ran on 2026-03-08; the next run was 2026-03-09T06:30Z.

The fix. One helper, next_wall_clock_run, replaces both arms (named zone and host-local zone). This function is the single choke point for every next-run caller. The helper:

  • walks the expression in wall-clock time;
  • resolves each match against the zone itself;
  • takes the first instant strictly after from;
  • for a time inside a spring-forward gap, runs at the first valid instant after the gap.

Spring-forward choice. For a time inside the gap there were two options: keep skipping (the cron crate's default), or run at the first valid instant after the gap. I took the second, which is what cronie does: 30 2 * * * on 2026-03-08 in New York runs at 03:00 EDT. The other choice is a one-line change if maintainers prefer skipping.

API Or Behavior Changes

  • No public API change.
  • Behaviour:
    • A cron next-run is now never at or before from.
    • In a spring-forward gap, a skipped time runs once at the first valid instant after the gap (e.g. 03:00) instead of being dropped for that day.
    • Fall-back behaviour is otherwise unchanged: a time in the repeated hour still matches on both passes, as before, just never in the past.

Tests

Regression tests (in schedule_tests.rs, cargo test -p tinyflows-schedule --lib -- <tests>):

  • cron_next_run_in_repeated_fall_back_hour_is_after_from: expects 2026-11-01T06:15Z.
  • cron_spring_forward_gap_does_not_skip_the_day: expects 2026-03-08T07:00Z (03:00 EDT).

Results:

  • Before (main source): 0 passed, 2 failed. The fall-back test got 05:15Z; the spring-forward test got 2026-03-09T06:30Z.
  • After: 2 passed.

Only those tests were run locally, per our process; CI runs the full matrix.

  • N/A: cargo fmt --check. I ran rustfmt on the touched files; CI runs the check.
  • N/A: cargo clippy --all-targets -- -D warnings. Left to CI.
  • N/A: cargo clippy --all-targets --all-features -- -D warnings. Left to CI.
  • N/A: cargo build --all-targets. Left to CI.
  • N/A: cargo build --all-targets --all-features. Left to CI.
  • N/A: cargo test. Only the targeted tests above were run; CI runs the full suite.
  • N/A: cargo test --all-features. Left to CI.

Documentation

The new helper's doc comment records the DST rules. No other docs were affected.

next_after took the first match from cron's iterator. In a repeated
fall-back hour that is the earlier instant, which can be before 'from',
so the next run landed in the past. A time inside a spring-forward gap
was silently dropped, so a daily job skipped the day the clocks moved.

Walk the expression in wall-clock time, resolve each match here, take the
first instant strictly after 'from', and run a skipped time at the first
valid instant after the gap (cronie's behaviour).
@coderabbitai

coderabbitai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 2 billable files and costs up to $0.50.

Or wait 30 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fa9a41e0-76c6-4da0-9547-2ecafa55e6dd
📥 Commits

Reviewing files that changed from the base of the PR and between 99fd391 and 7cd439c.

📒 Files selected for processing: 2
  • crates/tinyflows-schedule/src/schedule.rs
  • crates/tinyflows-schedule/src/schedule_tests.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@tinysweeper

tinysweeper Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Reviewed head: 7cd439cda6068d49f22edd12c744158afbd00bfd

Findings

  • high: Snap a skipped cron time to the next slot, not the next hour (crates/tinyflows-schedule/src/schedule.rs:233)
    Problem: When the expression's wall time falls in a spring-forward gap, this walks forward from `wall` in wall-clock minutes and takes the first resolvable local time. In a zone that shifts
    Suggested fix:
    No concrete fix was supplied.

Unverified notes — repository context was unavailable; these are not actionable findings

Unverified note — critique

  • high: Search from the UTC instant, not its wall-clock digits, as wall_from (crates/tinyflows-schedule/src/schedule.rs:227)
    Problem: `from` is a UTC instant; `next_run_for_schedule` callers pass `Utc::now()`. The code converts it to the zone, takes `naive_local()`, and re-labels those digits as UTC — the offset
    Suggested fix:
    No concrete fix was supplied.

Verdict: Changes requested · Priority: high
Coverage: Reviewed 2 of 2 files.

Diagnostics and supporting details

Full summary

Pull request purpose (title): fix(schedule): never schedule a cron run in the past or skip a DST day. Tiny Sweeper completed its review; deterministic results follow.

Review snapshot

Change surface Files Review signal Count
Production 1 Active findings 1
Tests 1 Noted findings 1
Documentation 0 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: Test coverage is assessed from changed tests and lane evidence; execution is not claimed without trusted check data.

Features

  • Internal refactor — next_wall_clock_run helper: Replaces both arms of `ScheduleTimeZone::next_after` with a single wall-clock walk that explicitly resolves ambiguous and skipped local times, taking the first instant strictly after `from`. (crates/tinyflows-schedule/src/schedule.rs impl<'a> CronPlan<'a> {)
  • Modified — Never schedule a cron run in the past during the repeated fall-back hour: In the repeated hour, both instants of an ambiguous wall-clock time are considered, so a run is not returned at or before `from` as the previous cron-crate ordering could do. (crates/tinyflows-schedule/src/schedule.rs impl ScheduleTimeZone {, crates/tinyflows-schedule/src/schedule_tests.rs fn validate_schedule_rejects_invalid_active_hours_end() {)
  • Modified — Run a spring-forward gap time at the first valid instant instead of skipping the day: A daily `30 2 * * *` expression that lands in the spring-forward gap now runs at the first valid instant after the gap rather than being dropped for that day. (crates/tinyflows-schedule/src/schedule.rs impl<'a> CronPlan<'a> {)

Tests

  • regression — `cron_next_run_in_repeated_fall_back_hour_is_after_from` sets `from = 2026-11-01T06:10Z` with `*/15 * * * *` in America/New_York and asserts the next run is `2026-11-01T06:15Z`, the second pass through the repeated hour.: Pins the fall-back expectation; the tests lane is Answered and reports a separate reachable defect in the gap branch, not in this case. (crates/tinyflows-schedule/src/schedule_tests.rs fn validate_schedule_rejects_invalid_active_hours_end() {)

Before merge

  • Address Snap a skipped cron time to the next slot, not the next hour (crates/tinyflows-schedule/src/schedule.rs).
Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Critique findings were capped at Likely confidence because retrieval was degraded: Re-check the cron expression after resolving a DST gap; Search from the UTC instant, not its wall-clock digits, as wall_from. (1 finding added by a second pass; 2 model findings retained at no more than Likely confidence because retrieval was degraded) _The code index is behind this pull request (indexed at `104f6da9a367`), so retrieved context may be out of date._

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The security lane is Answered with both files reviewed and found no security-relevant path or rule violation; it describes the change as bounded and self-contained.
  • Lane summary: The change replaces chrono's own timezone resolution in `ScheduleTimeZone::next_after` with a wall-clock walk that resolves ambiguous and skipped local times explicitly, with tests for the fall-back repeat and spring-forward gap. The logic is bounded and self-contained, and I found no security-relevant path or rule violation to report. _The code index is behind this pull request (indexed at `104f6da9a367`), so retrieved context may be out of date._

tests

  • Conclusion: Failure
  • Scope reviewed: all assigned evidence
  • Positive: The tests lane is Answered and confirms two new tests in `schedule_tests.rs` pin the exact expected instants for the fall-back and gap cases; it also reports one reachable defect in the gap branch.
  • Lane summary: The change rewrites cron next-run resolution to walk wall-clock times and resolve DST fall-back and spring-forward gaps explicitly, with two new tests in schedule_tests.rs that pin the exact expected instants for both cases. One reachable defect: the gap branch shifts past the requested wall time and can advance a run an hour beyond the intended slot when the gap minute is the last minute of an hour. _The code index is behind this pull request (indexed at `104f6da9a367`), so retrieved context may be out of date._
  • Evidence: crates/tinyflows-schedule/src/schedule.rs — Snap a skipped cron time to the next slot, not the next hour

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The commits lane is Answered and reports no sensitive content in committed files.
  • Lane summary: No sensitive content in committed files. _The code index is behind this pull request (indexed at `104f6da9a367`), so retrieved context may be out of date._

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The description lane is Answered and confirms the helper replaces both `next_after` arms and that the stated DST behaviour—never at or before `from`, gap resolved to the first valid instant after the gap, both passes of a repeated hour kept—matches the code and the two regression tests.
  • Lane summary: The helper replaces both `next_after` arms as the description says, and the stated DST behaviour (never at or before `from`, gap resolved to the first valid instant after the gap, both passes of a repeated hour kept) is what the code does; the two regression tests match the described expectations. I could not verify the chrono behaviour the fix relies on, but nothing in the diff contradicts the description. _The code index is behind this pull request (indexed at `104f6da9a367`), so retrieved context may be out of date._
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.000875
  • Tokens: 314766 input · 6915 output · 52224 cached · 243 embedding
  • Updated: 2026-10-22T21:41:31Z
Head State Pass summary
7cd439cda606 changes requested 1 active finding(s), 0 resolved finding(s) (at 2026-10-22T21:41:31Z)

tinysweeper 0.1.2

@tinysweeper tinysweeper 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.

Requesting changes: 1 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

Diagnostics

Total spend: $0.0009

@tinysweeper tinysweeper Bot added the priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition. label Oct 22, 2026
@M3gA-Mind
M3gA-Mind merged commit 2fa539b into tinyhumansai:main Oct 22, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant