You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Daily cron schedules run twice when daylight saving time ends #882
Fixed in Core 2.15.13 through #974. The normal 17-job published release check and installed six-case before/after acceptance pass. The daylight-saving rules are deployed and verified in all eight languages. #975 adds the 26 DST cases to fast PR feedback. Native handoff remains in Server #325; Sample App adoption is tracked separately in its #290.
Problem
A daily cron schedule in a timezone with daylight saving time runs twice on the day clocks go back.
30 1 * * * fires at 01:30 EDT and again at 01:30 EST on November 1. Each fire then computes the next time from now, so the schedule starts both runs.
30 2 * * * on March 8, when 02:30 doesn't exist, fires at 03:30 EDT.
The schedules guide accepts a timezone but doesn't say what happens to skipped or repeated wall-clock times. A once-a-day job, for example billing, runs twice a year with no warning.
Library behavior reproduced in an isolated container; the engine's schedule path wasn't run end to end.
Acceptance criteria
A wall-clock time that occurs twice fires once per scheduled occurrence. A time that doesn't exist fires once, by a documented rule (run at the next valid time, or skip).
Tests cover a daily schedule across both 2026 US transitions and one non-US timezone, plus a sub-hourly cron inside the repeated hour, through the engine's scheduler.
The schedules guide states the daylight saving rules and recommends UTC when a fixed interval matters.
Accepted as a scheduler product defect. The locked cron-library reproduction establishes the repeated local time; engine admission and fire identity still need an end-to-end counterexample before a runtime fix is claimed.
The next source step is an engine test covering the two UTC instants for the same daily wall-clock occurrence, plus spring-forward and a non-US transition. Deduplication must preserve distinct legitimate sub-hourly occurrences within the repeated hour and retain namespace/schedule identity, jitter, backfill and recovery behavior. Choose and document the skipped-time rule as part of the implementation, then update the schedules guide in every supported locale.
This remains open as product work, alongside the existing schedule identity/idempotency issues #874 and #876; it is not closed by recommending UTC. Existing P1 delivery remains first, and no competing scheduler implementation or new release is claimed.
Decided skipped-time rule: a wall-clock time that doesn't exist because clocks went forward fires once, moved forward by the gap. For example, 02:30 becomes 03:30 EDT. This keeps a daily job from silently losing a day, and it matches what the cron library already does.
Repeated-time rule: the time fires once, at its first (pre-transition) instant, whenever the cron hour field names specific hours. An hour field of * runs on elapsed time, so sub-hourly schedules keep their distinct occurrences in both passes of the repeated hour.
Reading the library source turned up a second double-fire. In the hour after clocks go back, HoursField::isSatisfiedBy also accepts the repeated hour as the next hour. As a result, 30 2 * * * fires at 01:30 EST as well as at 02:30 EST. #974 covers this case.
Remaining follow-up outside this repository: the schedules guide in durable-workflow.github.io should state these rules and recommend UTC when a fixed interval matters. I'll leave that open here until #974's behavior is qualified.
The schedule-guide follow-through is staged as documentation #418, exact cbcfd9090840375a303ecd486fa7f4a026aeafe6. It updates English and all seven translations, with only this guide's reviewed-source hashes changed. The full Node 24 production build passes all eight locales, strict translation checks, links, search and LLM manifests. The new section and all-hour examples render correctly in each of the eight schedule pages.
The draft remains held until #974 completes full qualification, publishes and passes installed embedded acceptance. General owns protected merge, the routine Core patch release, installed acceptance, this guide's delivery and the native contract handoff. The implementation owner continues source and before/after qualification. Independent bounded checks of the all-hour equivalence, fixed-offset fallback and exact fall-transition cases pass at current 04c2ce8a; its full run is still waiting on unit coverage. No PHP Server release is involved.
Fresh published-package baseline now reproduces the defect through the public schedule API. Core 2.15.12, source 7c80ffccdb6ab6e6ace8339618a9d6f1d6d9432f, was installed normally with Laravel 12.70.2, Testbench 10.12.0, PHPUnit 12.5.43 and PHP 8.3.36. Its 681 src/ files match the published tag before and after the experiment; no package code was overlaid.
Six ScheduleManager::create() / tick() cases across New York's November 1 fold give three expected product failures and three passing controls, with zero errors. Daily 30 1, 0 1 and 30 2 each admit two runs where the corrected contract expects one; the 30 2 case includes the early 06:30Z admission. Equivalent all-hour expressions 30 *, 30 */1 and 30 0-23 each correctly admit both distinct occurrences.
Scope is SQLite and supported database-queue configuration, with a fake queue preventing worker execution. This verifies real schedule admission and created run counts, not worker completion or restart recovery. The same locked consumer is retained for a minimal Core-only update to the qualified published patch and an identical after-run. Documentation #418 remains draft until that installed acceptance passes.
Source fix is qualified in #974 at exact head e0cb5dbbd1762caa662abcd49396787c28b19ba6. The before/after evidence, full-quality results and the rules it delivers are in #974 (final handoff comment). As agreed, General owns the protected merge, routine Core release, installed embedded acceptance, the schedules guide (docs #418) and the native #325 follow-through. This issue should close after the published fix passes installed acceptance.
Core 2.15.13 is published from protected merge 42be75605a6e4c35e0e2bade52086a62756c3e30 (annotated tag object 1915ce2f3e5c624defb36ed1f8adfe40fcd2825e). Release. Packagist source and dist both resolve to that commit. The normal release verification passed all 17 jobs: published package contract plus the supported Laravel/PHP upgrade matrix.
Installed before/after acceptance also passes. A normal Composer update changed only Core 2.15.12 → 2.15.13; the other 117 packages stayed identical. All 681 installed Core source files match the published tag before and after execution. The unchanged six-case fixture now passes 6 tests / 27 assertions, with zero errors or admission refusals. The three daily New York schedules (30 1, 0 1, 30 2) each create one run; the three all-hour forms (*, */1, 0-23) retain both repeated-hour occurrences. The published 2.15.12 baseline reproduced three failures, each creating two daily runs.
Scope: PHP 8.3.36, Laravel 12.70.2, SQLite in memory, public schedule create/tick with the supported database queue and a queue fake. This proves schedule admission and run creation, not worker completion. Source/full database qualification remains linked in #974.
The eight-language schedule guide is qualified in durable-workflow/durable-workflow.github.io#418 and is proceeding to protected merge and public-site verification. #882 remains open through that delivery. No PHP Server release is part of this change.
Delivered in published Core 2.15.13. Publication, 17-job release verification and installed before/after acceptance are complete. The source regression covers both US transitions, Kyiv, sub-hourly behavior, equivalent every-hour expressions and the exact transition instant; the installed six-case fixture proves corrected schedule admission with an otherwise unchanged dependency graph.
The schedule guide is now live in all eight languages. Docs PR durable-workflow/durable-workflow.github.io#418 merged as ef9ee70c542d8dd8e789016a345be4c9470d703c; both the normal deploy run 41420734048 and Pages run 41421364473 passed. Fresh 09:33 UTC public GETs verify all eight canonical pages, languages, daylight-saving sections and all-hour examples.
CI follow-through #975 is also merged: all 26 DST cases now run in the PR unit slice (530→556 tests, 15.25→15.40 seconds) without changing full-suite gates. Native contract handoff is recorded in Server #325; native source and qualification remain with that owner. Sample App adoption continues separately in durable-workflow/sample-app#290 / PR #291.
The embedded defect, published installation check and documented behavior are complete, so this issue can close.
Activity
rmcdaniel commentedon Oct 21, 2026
Accepted as a scheduler product defect. The locked cron-library reproduction establishes the repeated local time; engine admission and fire identity still need an end-to-end counterexample before a runtime fix is claimed.
The next source step is an engine test covering the two UTC instants for the same daily wall-clock occurrence, plus spring-forward and a non-US transition. Deduplication must preserve distinct legitimate sub-hourly occurrences within the repeated hour and retain namespace/schedule identity, jitter, backfill and recovery behavior. Choose and document the skipped-time rule as part of the implementation, then update the schedules guide in every supported locale.
This remains open as product work, alongside the existing schedule identity/idempotency issues #874 and #876; it is not closed by recommending UTC. Existing P1 delivery remains first, and no competing scheduler implementation or new release is claimed.
rmcdaniel commentedon Nov 2, 2026
Draft fix: #974.
Decided skipped-time rule: a wall-clock time that doesn't exist because clocks went forward fires once, moved forward by the gap. For example, 02:30 becomes 03:30 EDT. This keeps a daily job from silently losing a day, and it matches what the cron library already does.
Repeated-time rule: the time fires once, at its first (pre-transition) instant, whenever the cron hour field names specific hours. An hour field of
*runs on elapsed time, so sub-hourly schedules keep their distinct occurrences in both passes of the repeated hour.Reading the library source turned up a second double-fire. In the hour after clocks go back,
HoursField::isSatisfiedByalso accepts the repeated hour as the next hour. As a result,30 2 * * *fires at 01:30 EST as well as at 02:30 EST. #974 covers this case.Remaining follow-up outside this repository: the schedules guide in
durable-workflow.github.ioshould state these rules and recommend UTC when a fixed interval matters. I'll leave that open here until #974's behavior is qualified.rmcdaniel commentedon Nov 2, 2026
The schedule-guide follow-through is staged as documentation #418, exact
cbcfd9090840375a303ecd486fa7f4a026aeafe6. It updates English and all seven translations, with only this guide's reviewed-source hashes changed. The full Node 24 production build passes all eight locales, strict translation checks, links, search and LLM manifests. The new section and all-hour examples render correctly in each of the eight schedule pages.The draft remains held until #974 completes full qualification, publishes and passes installed embedded acceptance. General owns protected merge, the routine Core patch release, installed acceptance, this guide's delivery and the native contract handoff. The implementation owner continues source and before/after qualification. Independent bounded checks of the all-hour equivalence, fixed-offset fallback and exact fall-transition cases pass at current
04c2ce8a; its full run is still waiting on unit coverage. No PHP Server release is involved.rmcdaniel commentedon Nov 2, 2026
Fresh published-package baseline now reproduces the defect through the public schedule API. Core 2.15.12, source
7c80ffccdb6ab6e6ace8339618a9d6f1d6d9432f, was installed normally with Laravel 12.70.2, Testbench 10.12.0, PHPUnit 12.5.43 and PHP 8.3.36. Its 681src/files match the published tag before and after the experiment; no package code was overlaid.Six
ScheduleManager::create()/tick()cases across New York's November 1 fold give three expected product failures and three passing controls, with zero errors. Daily30 1,0 1and30 2each admit two runs where the corrected contract expects one; the30 2case includes the early 06:30Z admission. Equivalent all-hour expressions30 *,30 */1and30 0-23each correctly admit both distinct occurrences.Scope is SQLite and supported database-queue configuration, with a fake queue preventing worker execution. This verifies real schedule admission and created run counts, not worker completion or restart recovery. The same locked consumer is retained for a minimal Core-only update to the qualified published patch and an identical after-run. Documentation #418 remains draft until that installed acceptance passes.
rmcdaniel commentedon Nov 2, 2026
Source fix is qualified in #974 at exact head
e0cb5dbbd1762caa662abcd49396787c28b19ba6. The before/after evidence, full-quality results and the rules it delivers are in #974 (final handoff comment). As agreed, General owns the protected merge, routine Core release, installed embedded acceptance, the schedules guide (docs #418) and the native #325 follow-through. This issue should close after the published fix passes installed acceptance.rmcdaniel commentedon Nov 2, 2026
Core 2.15.13 is published from protected merge
42be75605a6e4c35e0e2bade52086a62756c3e30(annotated tag object1915ce2f3e5c624defb36ed1f8adfe40fcd2825e). Release. Packagist source and dist both resolve to that commit. The normal release verification passed all 17 jobs: published package contract plus the supported Laravel/PHP upgrade matrix.Installed before/after acceptance also passes. A normal Composer update changed only Core 2.15.12 → 2.15.13; the other 117 packages stayed identical. All 681 installed Core source files match the published tag before and after execution. The unchanged six-case fixture now passes 6 tests / 27 assertions, with zero errors or admission refusals. The three daily New York schedules (
30 1,0 1,30 2) each create one run; the three all-hour forms (*,*/1,0-23) retain both repeated-hour occurrences. The published 2.15.12 baseline reproduced three failures, each creating two daily runs.Scope: PHP 8.3.36, Laravel 12.70.2, SQLite in memory, public schedule create/tick with the supported database queue and a queue fake. This proves schedule admission and run creation, not worker completion. Source/full database qualification remains linked in #974.
The eight-language schedule guide is qualified in durable-workflow/durable-workflow.github.io#418 and is proceeding to protected merge and public-site verification. #882 remains open through that delivery. No PHP Server release is part of this change.
rmcdaniel commentedon Nov 2, 2026
Delivered in published Core 2.15.13. Publication, 17-job release verification and installed before/after acceptance are complete. The source regression covers both US transitions, Kyiv, sub-hourly behavior, equivalent every-hour expressions and the exact transition instant; the installed six-case fixture proves corrected schedule admission with an otherwise unchanged dependency graph.
The schedule guide is now live in all eight languages. Docs PR durable-workflow/durable-workflow.github.io#418 merged as
ef9ee70c542d8dd8e789016a345be4c9470d703c; both the normal deploy run 41420734048 and Pages run 41421364473 passed. Fresh 09:33 UTC public GETs verify all eight canonical pages, languages, daylight-saving sections and all-hour examples.CI follow-through #975 is also merged: all 26 DST cases now run in the PR unit slice (530→556 tests, 15.25→15.40 seconds) without changing full-suite gates. Native contract handoff is recorded in Server #325; native source and qualification remain with that owner. Sample App adoption continues separately in durable-workflow/sample-app#290 / PR #291.
The embedded defect, published installation check and documented behavior are complete, so this issue can close.