Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions system/async/tasks/ScheduledTask.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,13 @@ component accessors="true" {
}
}

// If this is an interval-based (every()) task with an explicit daily start time and no
// initial delay was set some other way, align the first execution to the next period
// boundary counted from that start time instead of firing immediately on registration.
if ( variables.period > 0 && variables.delay == 0 && len( variables.startTime ) ) {
calculateStartTimeAlignedDelay();
}

debugLog(
"start",
{
Expand Down Expand Up @@ -1550,6 +1557,43 @@ component accessors="true" {
variables.timeUnit = "seconds";
}

/**
* When an interval-based task ( every() ) has an explicit daily start time
* ( startOnTime() / between() ) but no initial delay was set some other way, this
* calculates an initial delay that aligns the first execution to the next period
* boundary counted from that start time, instead of firing immediately on registration.
*/
private function calculateStartTimeAlignedDelay(){
var now = getJavaNow();
var anchor = now
.withHour( javacast( "int", getToken( variables.startTime, 1, ":" ) ) )
.withMinute( javacast( "int", getToken( variables.startTime, 2, ":" ) ) )
.withSecond( javacast( "int", 0 ) )
.withNano( javacast( "int", 0 ) );

var periodSeconds = variables.timeUnitHelper
.get( variables.timeUnit )
.toSeconds( javacast( "long", variables.period ) );

if ( periodSeconds <= 0 ) {
return;
}

var jDuration = variables.dateTimeHelper.duration().getNative();
var elapsedSeconds = jDuration.between( anchor, now ).getSeconds();
if ( elapsedSeconds > 0 ) {
var periodsElapsed = int( elapsedSeconds / periodSeconds ) + 1;
anchor = anchor.plusSeconds( javacast( "long", periodsElapsed * periodSeconds ) );
}

// Set delay/period directly (in seconds), matching the smart every*At() helpers
variables.delay = jDuration.between( now, anchor ).getSeconds();
variables.delayTimeUnit = "seconds";
variables.period = periodSeconds;
variables.timeUnit = "seconds";
variables.stats.nextRun = anchor.toString();
}

/**
* This method is called to set the next run time of the task based on the timeUnit and period.
*/
Expand Down
26 changes: 26 additions & 0 deletions tests/specs/async/tasks/ScheduledTaskSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,32 @@ component extends="tests.specs.async.BaseAsyncSpec" {
expect( t.getPeriod() ).toBe( 31536000 );
expect( t.getTimeUnit() ).toBe( "seconds" );
} );

it( "can align every() + startOnTime() to the next period boundary instead of firing immediately", function(){
var t = scheduler
.task( "test" )
.every( 1800, "seconds" )
.startOnTime( "00:00" );
t.start();
expect( t.getDelay() ).toBeGT( 0 );
expect( t.getDelay() ).toBeLTE( 1800 );
} );

it( "every() without startOnTime() still fires immediately (unchanged behavior)", function(){
var t = scheduler.task( "test" ).every( 1800, "seconds" );
t.start();
expect( t.getDelay() ).toBe( 0 );
} );

it( "explicit delay() takes precedence over startOnTime() alignment", function(){
var t = scheduler
.task( "test" )
.every( 1800, "seconds" )
.startOnTime( "00:00" )
.delay( 5, "seconds", true );
t.start();
expect( t.getDelay() ).toBe( 5 );
} );
} );

describe( "can register frequencies with constraints", function(){
Expand Down
Loading