Scheduled Tasks
The ColdBox Scheduled Tasks offers a fresh, programmatic and human approach to scheduling tasks on your server and multi-server application

Introduction

Scheduled tasks have always been a point of soreness for many developers in ANY language. Especially choosing where to place them for execution: should it be cron? windows task scheduler? ColdFusion engine? Jenkins, Gitlab? and the list goes on and on.
ColdBox Scheduled Tasks
The ColdBox Scheduled Tasks offers a fresh, programmatic and human approach to scheduling tasks on your server and multi-server application. It allows you to define your tasks in a portable Scheduler we lovingly call the Scheduler.cfc which not only can be used to define your tasks, but also monitor all of their life-cycles and metrics of tasks. Since ColdBox is also hierarchical, it allows for every single ColdBox Module to also define a Scheduler and register their own tasks as well. This is a revolutionary approach to scheduling tasks in an HMVC application.
The ColdBox Scheduler is built on top of the core async package Scheduler.

Global App Scheduler

Every ColdBox application has a global scheduler created for you by convention and registered with a WireBox ID of [email protected]. However, you can have complete control of the scheduler by creating the following file: config/Scheduler.cfc. This is a simple CFC with a configure() method where you will define your tasks and several life-cycle methods.
config/Scheduler.cfc
1
component {
2
3
/**
4
* Configure the ColdBox Scheduler
5
*/
6
function configure() {
7
/**
8
* --------------------------------------------------------------------------
9
* Configuration Methods
10
* --------------------------------------------------------------------------
11
* From here you can set global configurations for the scheduler
12
* - setTimezone( ) : change the timezone for ALL tasks
13
* - setExecutor( executorObject ) : change the executor if needed
14
* - setCacheName( "template" ) : Change the cachename for ALL tasks
15
* - setServerFixation( true ) : Set all tasks to run on one server
16
*/
17
18
19
20
/**
21
* --------------------------------------------------------------------------
22
* Register Scheduled Tasks
23
* --------------------------------------------------------------------------
24
* You register tasks with the task() method and get back a ColdBoxScheduledTask object
25
* that you can use to register your tasks configurations.
26
*/
27
28
task( "Clear Unregistered Users" )
29
.call( () => getInstance( "UserService" ).clearRecentUsers() )
30
.everyDayAt( "09:00" );
31
32
task( "Hearbeat" )
33
.call( () => runEvent( "main.heartbeat" ) )
34
.every( 5, "minutes" )
35
.onFailure( ( task, exception ) => {
36
getInstance( "System" ).sendBadHeartbeat( exception );
37
} );
38
}
39
40
/**
41
* Called before the scheduler is going to be shutdown
42
*/
43
function onShutdown(){
44
}
45
46
/**
47
* Called after the scheduler has registered all schedules
48
*/
49
function onStartup(){
50
}
51
52
/**
53
* Called whenever ANY task fails
54
*
55
* @task The task that got executed
56
* @exception The ColdFusion exception object
57
*/
58
function onAnyTaskError( required task, required exception ){
59
}
60
61
/**
62
* Called whenever ANY task succeeds
63
*
64
* @task The task that got executed
65
* @result The result (if any) that the task produced
66
*/
67
function onAnyTaskSuccess( required task, result ){
68
}
69
70
/**
71
* Called before ANY task runs
72
*
73
* @task The task about to be executed
74
*/
75
function beforeAnyTask( required task ){
76
}
77
78
/**
79
* Called after ANY task runs
80
*
81
* @task The task that got executed
82
* @result The result (if any) that the task produced
83
*/
84
function afterAnyTask( required task, result ){
85
}
86
87
}
Copied!

Life-Cycle Methods

Every Scheduler can create life-cycle methods and monitor the scheduled tasks:
Method
Description
onStartup()
Called after the scheduler has registered all schedules
onShutdown()
Called before the scheduler is going to be shutdown
onAnyTaskError(task,exception)
Called whenever ANY task fails
onAnyTaskSuccess(task,result)
Called whenever ANY task succeeds
beforeAnyTask(task)
Called before ANY task runs
afterAnyTask(task,result)
Called after ANY task runs

Configuration Methods

The following methods are used to impact the operation of all scheduled tasks managed by the scheduler:
Method
Description
setCacheName( cacheName )
Set the cachename to use for all registered tasks
setServerFixation( boolean )
Set the server fixation to use for all registered tasks
setTimezone( timezone )
Set the timezone to use for all registered tasks
setExecutor( executor )
Override the executor generated for the scheduler

Cachename For All Tasks

By default, all tasks are fixed to use the template cache when doing server fixation. You can override the cachename by a task by task basis or set the global default into the scheduler.
1
setCacheName( "Redis" )
Copied!

Server Fixation For All Tasks

By default, all task run on each server/container they are registered with. However, you can also pin them on a specific server using server fixation via the onOneServer() method of the individual scheduled task. However, you can also tell the scheduler to do this for ALL tasks it manages using the setServerFixation() method.
1
setServerFixation( true )
Copied!

Timezone For All Tasks

By default, all tasks run under the system default timezone which usually is UTC. However, if you would like to change to a different execution timezone, then you can use the setTimeZone() method and pass in a valid timezone string:
1
setTimezone( "America/Chicago" )
Copied!
Remember that some timezones utilize daylight savings time. When daylight saving time changes occur, your scheduled task may run twice or even not run at all. For this reason, we recommend avoiding timezone scheduling when possible.

Custom Executor

By default the scheduler will register a scheduled executor with a default of 20 threads for you with a name of [email protected] If you want to add in your own executor as per your configurations, then just call the setExecutor() method.
1
setExecutor(
2
getAsyncManager().newScheduledExecutor( "mymymy", 50 )
3
);
Copied!
You can find how to work with executors in our executors section.

Scheduler Properties

Every scheduler has the following injections available to you in the variables scope
Object
Description
asyncManager
Async manager reference
cachebox
CacheBox reference
cacheName
The name of the cache for server fixation and more for all tasks
controller
ColdBox controller reference
executor
Scheduled executor
log
A pre-configured log object
started
A boolean flag indicating if the scheduler has started or not
serverFixation
The boolean flag that indicates the default for server fixation for all tasks
tasks
The collection of registered tasks
timezone
Java based timezone object
util
ColdBox utility
wirebox
WireBox reference

Scheduler ColdBox Methods

Every scheduler has several useful ColdBox interaction methods you can use when registering your tasks callable methods.
Method
Description
announce()
Announce an interception
externalView()
Render an external view
getCache()
Get a cache from CacheBox
getColdBoxSetting()
Get a ColdBox setting
getEnv()
Retrieve a environment variable only
getInstance()
Get a instance object from WireBox
getModuleConfig()
Get a module config
getModuleSettings()
Get a module setting
getRenderer()
Get the ColdBox Renderer
getSetting()
Get an app Setting
getSystemSetting()
Retrieve a Java System property or env value by name. It looks at properties first then environment variables
getSystemProperty()
Retrieve a Java System property only
layout()
Render a layout
locateDirectoryPath()
Resolve a directory to be either relative or absolute in your application
locateFilePath()
Resolve a file to be either relative or absolute in your application
runEvent()
Run a ColdBox Event
runRoute()
Run a ColdBox Route
settingExists()
Check if a setting exists
setSetting()
Set a setting
view()
Render a view

Scheduler Utility Methods

Every scheduler has several utility methods:
Method
Description
getRegisteredTasks()
Get an ordered array of all the tasks registered in the scheduler
getTaskRecord( name )
Get the task record structure by name:
{
name,
task,
future,
scheduledAt,
registeredAt,
error,
errorMessage,
stacktrace
}
getTaskStats()
Builds out a struct report for all the registered tasks in this scheduler
hasTask( name )
Check if a scheduler has a task registered by name
hasStarted()
Has the scheduler started already
removeTask( name )
Cancel a task and remove it from the scheduler
startup()
Startup the scheduler. This is called by ColdBox for you. No need to call it.
shutdown()
Shutdown the scheduler
task( name )
Register a new task and return back to you the task so you can build it out.

Scheduling Tasks

Ok, now that we have seen all the capabilities of the scheduler, let's dive deep into scheduling tasks with the task( name ) method.

Registering Tasks

Once you call on this method, the scheduler will create a ColdBoxScheduledTask object for you, configure it, wire it, register it and return it to you.
1
task( "my-task" )
Copied!

Task Closure/Lambda/Object

You register the callable event via the call() method on the task object. You can register a closure/lambda or a invokable CFC. If you register an object, then we will call on the object's run() method by default, but you can change it using the method argument and call any public/remote method.
1
// Lambda Syntax
2
task( "my-task" )
3
.call( () => getInstance( "myService" ).runcleanup() )
4
.everyHour();
5
6
// Closure Syntax
7
task( "my-task" )
8
.call( function(){
9
// task here
10
} )
11
.everyHourAt( 45 );
12
13
// Object with run() method
14
task( "my-task" )
15
.call( getInstance( "MyTask" ) )
16
.everyDay()
17
18
// Object with a custom method
19
task( "my-task" )
20
.call( getInstance( "CacheService" ), "reapCache" )
21
.everydayAt( "13:00" )
Copied!

Frequencies

There are many many frequency methods in ColdBox scheduled tasks that will enable the tasks in specific intervals. Every time you see that an argument receives a timeUnit the available options are:
  • days
  • hours
  • minutes
  • seconds
  • milliseconds (default)
  • microseconds
  • nanoseconds
Ok, let's go over the frequency methods:
Frequency Method
Description
every( period, timeunit )
Run the task every custom period of execution
spacedDelay( spacedDelay, timeunit )
Run the task every custom period of execution but with NO overlaps
everyMinute()
Run the task every minute from the time it get's scheduled
everyHour()
Run the task every hour from the time it get's scheduled
everyHourAt( minutes )
Set the period to be hourly at a specific minute mark and 00 seconds
everyDay()
Run the task every day at midnight
everyDayAt( time )
Run the task daily with a specific time in 24 hour format: HH:mm
everyWeek()
Run the task every Sunday at midnight
everyWeekOn( day, time )
Run the task weekly on the given day of the week and time
everyMonth()
Run the task on the first day of every month at midnight
everyMonthOn( day, time )
Run the task every month on a specific day and time
onFirstBusinessDayOfTheMonth( time )
Run the task on the first Monday of every month
onLastBusinessDayOfTheMonth( time )
Run the task on the last business day of the month
everyYear()
Run the task on the first day of the year at midnight
everyYearOn( month, day, time )
Set the period to be weekly at a specific time at a specific day of the week
onWeekends( time )
Run the task on Saturday and Sunday
onWeekdays( time )
Run the task only on weekdays at a specific time.
onMondays( time )
Only on Mondays
onTuesdays( time )
Only on Tuesdays
onWednesdays( time )
Only on Wednesdays
onThursdays( time )
Only on Thursdays
onFridays( time )
Only on Fridays
onSaturdays( time )
Only on Saturdays
onSundays( time )
Only on Sundays
All time arguments are defaulted to midnight (00:00)

Preventing Overlaps

Tasks with a fixed frequency vs delayed frequency
By default all tasks that have interval rates/periods that will execute on that interval schedule. However, what happens if a task takes longer to execute than the period? Well, by default the task will execute even if the previous one has not executed. If you want to prevent this behavior, then you can use the withNoOverlaps() method and ColdBox will register the tasks with a fixed delay. Meaning the intervals do not start counting until the last task has finished executing.
Task With Fixed Delay
1
task( "test" )
2
.call( () => getInstance( "CacheService" ).reap() )
3
.everyMinute()
4
.withNoOverlaps();
Copied!
Spaced delays are a feature of the Scheduled Executors. There is even a spacedDelay( delay, timeUnit ) method in the Task object.

Delaying First Execution

Every task can also have an initial delay of first execution by using the delay() method.
1
/**
2
* Set a delay in the running of the task that will be registered with this schedule
3
*
4
* @delay The delay that will be used before executing the task
5
* @timeUnit The time unit to use, available units are: days, hours, microseconds, milliseconds, minutes, nanoseconds, and seconds. The default is milliseconds
6
*/
7
ScheduledTask function delay( numeric delay, timeUnit = "milliseconds" )
Copied!
The delay is numeric and the timeUnit can be:
  • days
  • hours
  • minutes
  • seconds
  • milliseconds (default)
  • microseconds
  • nanoseconds
1
// Lambda Syntax
2
task( "my-task" )
3
.call( () => getInstance( "myService" ).runcleanup() )
4
.delay( "5000" )
5
.everyHour();
Copied!
Please note that the delay pushes the execution of the task into the future only for the first execution.

One Off Tasks

A part from registering tasks that have specific intervals/frequencies you can also register tasks that can be executed ONCE ONLY. These are great for warming up caches, registering yourself with control planes, setting up initial data collections and so much more.
Basically, you don't register a frequency just the callable event. Usually, you can also combine them with a delay of execution, if you need them to fire off after certain amount of time has passed.
1
task( "build-up-cache" )
2
.call( () => getInstance( "DataServices" ).buildCache() )
3
.delay( 1, "minutes" );
4
5
task( "notify-admin-server-is-up" )
6
.call( () => getInstance( "SettingService" ).notifyAppIsUp( getUtil().getServerIp() ) )
7
.delay( 30, "seconds" );
8
9
task( "register-container" )
10
.call( () => runEvent( "tasks.registerContainer" ) )
11
.delay( 30, "seconds" );
Copied!

Life-Cycle Methods

We already saw that a scheduler has life-cycle methods, but a task can also have several useful life-cycle methods:
Method
Description
after( target )
Store the closure to execute after the task executes
function( task, results )
before( target )
Store the closure to execute before the task executes
function( task )
onFailure( target )
Store the closure to execute if there is a failure running the task
function( task, exception )
onSuccess( target )
Store the closure to execute if the task completes successfully
function( task, results )
1
task( "testharness-Heartbeat" )
2
.call( function() {
3
if ( randRange(1, 5) eq 1 ){
4
throw( message = "I am throwing up randomly!", type="RandomThrowup" );
5
}
6
writeDump( var='====> I am in a test harness test schedule!', output="console" );
7
} )
8
.every( "5", "seconds" )
9
.before( function( task ) {
10
writeDump( var='====> Running before the task!', output="console" );
11
} )
12
.after( function( task, results ){
13
writeDump( var='====> Running after the task!', output="console" );
14
} )
15
.onFailure( function( task, exception ){
16
writeDump( var='====> test schedule just failed!! #exception.message#', output="console" );
17
} )
18
.onSuccess( function( task, results ){
19
writeDump( var="====> Test scheduler success : Stats: #task.getStats().toString()#", output="console" );
20
} );
Copied!

Timezone

By default, all tasks will ask the scheduler for the timezone to run in. However, you can override it on a task-by-task basis using the setTimezone( timezone ) method:
1
setTimezone( "America/Chicago" )
Copied!
Remember that some timezones utilize daylight savings time. When daylight saving time changes occur, your scheduled task may run twice or even not run at all. For this reason, we recommend avoiding timezone scheduling when possible.

Truth Test Constraints

There are many ways to constrain the execution of a task. However, you can register a when() closure that will be executed at runtime and boolean evaluated. If true, then the task can run, else it is disabled.
1
task( "my-task" )
2
.call( () => getInstance( "securityService" ).cleanOldUsers() )
3
.daily()
4
.when( function(){
5
// Can we run this task?
6
return true;
7
);
Copied!

Server Fixation

If you are running a cluster of your application and you register tasks they will run at their schedule in EVERY server/container the application has been deployed to. This might not be a great idea if you want only ONE task to run no matter how many servers/containers you have deployed your application on. For this situation you can use the onOneServer() method which tells ColdBox to ONLY run the task once on the first server that wins the race condition.
1
task( "my-task" )
2
.call( () => getInstance( "securityService" ).cleanOldUsers() )
3
.daily()
4
.onOneServer();
Copied!
This feature ONLY works when you are using a distributed cache like redis, mongo, elastic, couchbase or a JDBC CacheBox provider in CacheBox.

Changing the Cache Provider

By default this feature leverages the template cache provider in CacheBox. However, you can change which cache provider will be used for storing the locking and tracking entries.
1
task( "my-task" )
2
.call( () => getInstance( "securityService" ).cleanOldUsers() )
3
.daily()
4
.setCacheName( "redis" )
5
.onOneServer();
Copied!

Environment Constraints

All ColdBox applications have a runnable environment stored in the environment setting. You can use that to register a task with constraints of environment using the onEnvironment( environment ) method. This means that the task will ONLY run on those environments. The environment argument can be a single string, a list of environments or an array of environments.
1
task( "my-task" )
2
.call( () => getInstance( "securityService" ).cleanOldUsers() )
3
.daily()
4
.onEnvironment( "staging" );
5
6
task( "my-task" )
7
.call( () => getInstance( "securityService" ).cleanOldUsers() )
8
.daily()
9
.onEnvironment( [ "staging", "production" ] );
10
11
task( "my-task" )
12
.call( () => getInstance( "securityService" ).cleanOldUsers() )
13
.daily()
14
.onEnvironment( "staging,production" );
Copied!

Disabling/Pausing Tasks

Every task is runnable from registration according to the frequency you set. However, you can manually disable a task using the disable() method:
1
task( "my-task" )
2
.call( () => getInstance( "securityService" ).cleanOldUsers() )
3
.daily()
4
.disable();
Copied!
Once you are ready to enable the task, you can use the enable() method:
1
myTask.enable()
Copied!

Task Stats

All tasks keep track of themselves and have lovely metrics. You can use the getStats() method to get a a snapshot structure of the stats in time. Here is what you get in the stats structure:
Metric
Description
created
The timestamp of when the task was created in memory
inetHost
The hostname of the machine this task is registered with
lastRun
The last time the task ran
lastResult
The last result the task callable produced
localIp
The ip address of the server this task is registered with
neverRun
A boolean flag indicating if the task has NEVER been ran
nextRun
When the task will run next
totalFailures
How many times the task has failed execution
totalRuns
How many times the task has run
totalSuccess
How many times the task has run and succeeded
1
/**
2
* Called after ANY task runs
3
*
4
* @task The task that got executed
5
* @result The result (if any) that the task produced
6
*/
7
function afterAnyTask( required task, result ){
8
log.info( "task #task.getName()# just ran. Metrics: #task.getStats().toString()# ");
9
}
Copied!

Task Helpers

We have created some useful methods that you can use when working with asynchronous tasks:
Method
Description
err( var )
Send output to the error stream
getCache()
Get the CacheBox provider assigned for server fixation
getCacheName()
Get the name of the cache region to use for server fixation
getEnvironments()
Get the assigned running environments for the task
getServerFixation()
Get the boolean flag that indicates that this task runs on all or one server
hasScheduler()
Verifies if the task is assigned a scheduler or not
isDisabled()
Verifies if the task has been disabled by bit
isConstrained()
Verifies if the task has been constrained to run by server fixation, environments, weekends, weekdays, dayOfWeek, or dayOfMonth
out( var )
Send output to the output stream
setCacheName()
Set the cache name to use for server fixation
start()
This kicks off the task into the scheduled executor manually. This method is called for you by the scheduler upon application startup or module loading.

Schedulers For Modules

Every module in ColdBox also has a convention of config/Scheduler.cfc that if detected will register that scheduler for you with a WireBox ID of [email protected]{moduleName}. ColdBox will register the scheduler for you and also store it in the module's configuration struct with a key of scheduler. ColdBox will also manage it's lifecycle and destroy it if the module is unloaded. All the rules for schedulers apply, happy scheduling!
1
+ MyModule
2
+ config
3
- Router.cfc
4
- Scheduler.cfc
Copied!
Last modified 2mo ago