Skip to content

fix: align every()+startOnTime()/between() to next period boundary instead of firing immediately - #687

Merged
lmajano merged 2 commits into
developmentfrom
fix/scheduler-every-starttime-alignment
Aug 24, 2026
Merged

fix: align every()+startOnTime()/between() to next period boundary instead of firing immediately#687
lmajano merged 2 commits into
developmentfrom
fix/scheduler-every-starttime-alignment

Conversation

@lmajano

@lmajano lmajano commented Aug 24, 2026

Copy link
Copy Markdown
Member

Description

A scheduled task built with every(period).startOnTime("HH:mm") (or .between(startTime, endTime)) fired immediately the moment the scheduler registered it (e.g. on every server/container restart) instead of waiting for the next period boundary counted from the daily start time.

Example: every(1800, "seconds").startOnTime("00:00") should wait until the next 30-minute boundary from midnight (00:00, 00:30, 01:00, ...) — instead it fired right away at registration time, every time.

Root cause: every() only sets variables.period/variables.timeUnit, never variables.delay (defaults to 0). startTime/endTime are only consulted by isConstrained() as a runtime gate on later ticks — not fed into the initial delay calculation — so start() handed delay=0 straight to scheduleAtFixedRate(). The "smart" everyDayAt()/everyHourAt()/etc. methods already compute a real future first-run time via setInitialDelayPeriodAndTimeUnit(), but that pattern was never wired up for every() + startOnTime()/between().

Fix: system/async/tasks/ScheduledTask.cfcstart() now computes an initial delay aligned to the next period boundary from startTime when period > 0, delay == 0, and startTime is set, via a new private calculateStartTimeAlignedDelay() method that mirrors the existing everyDayAt()/setInitialDelayPeriodAndTimeUnit() pattern. Tasks with an explicit delay() already set, or one-off schedule() tasks, are unaffected.

This mirrors the identical fix already merged in ortus-boxlang/BoxLang PR #606 (Jira BL-2633).

Note: this is a behavior change for anyone currently relying on the old "fire immediately at registration" semantics for every() + startOnTime()/between().

Jira Issues

Bug Tracker: https://ortussolutions.atlassian.net/jira/software/c/projects/COLDBOX/issues

Type of change

  • Bug Fix
  • Improvement
  • New Feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist

  • My code follows the style guidelines of this project cfformat (hand-matched to surrounding code; box run-script format:check was not run — CommandBox wasn't available in the environment this PR was prepared in)
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes (not run locally — CommandBox/a live CF engine server wasn't available in the environment this PR was prepared in; please run tests/specs/async/tasks/ScheduledTaskSpec.cfc via TestBox before merge)

…stead of firing immediately

A scheduled task built with every(period).startOnTime("HH:mm") (or
.between(startTime, endTime)) fired immediately the moment the scheduler
registered it (e.g. on every server/container restart) instead of waiting
for the next period boundary counted from the daily start time.

Root cause: every() only set variables.period/timeUnit, never
variables.delay (defaults to 0). startTime/endTime were only consulted by
isConstrained() as a runtime gate on later ticks, not fed into the initial
delay calculation, so start() handed delay=0 straight to
scheduleAtFixedRate().

Fix mirrors the existing "smart" everyDayAt()/setInitialDelayPeriodAndTimeUnit()
pattern: start() now computes an initial delay aligned to the next period
boundary from startTime when period > 0, delay == 0, and startTime is set.
Tasks with an explicit delay() or one-off schedule() are unaffected.

This is a behavior change for anyone relying on the old "fire immediately
at registration" semantics for every()+startOnTime()/between().

Mirrors the identical fix already merged in ortus-boxlang/BoxLang PR #606
(Jira BL-2633).
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

Test Results

0 tests  ±0   0 ✅ ±0   0s ⏱️ ±0s
0 suites ±0   0 💤 ±0 
0 files   ±0   0 ❌ ±0 

Results for commit 3c006e6. ± Comparison against base commit 901ceef.

♻️ This comment has been updated with latest results.

CI's Checks Source Code Formatting job flagged two formatting violations
introduced by the previous commit: a misaligned consecutive assignment in
calculateStartTimeAlignedDelay(), and method chains in the new tests that
needed to wrap per the project's method_call.chain.multiline rule.

Verified locally with the project's own cfformat (./.cfformat.json) - only
a pre-existing, unrelated violation in system/web/routing/Router.cfc
remains, which also fails on development and is out of scope here.
@lmajano
lmajano merged commit 2f9fbd2 into development Aug 24, 2026
28 checks passed
@lmajano
lmajano deleted the fix/scheduler-every-starttime-alignment branch August 24, 2026 18:04
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