Task manager - pre-defined scheduled tasks

As of v10.7.0, Preside comes with an built-in task management system designed for running and monitoring scheduled and ad-hoc tasks in the system. For example, you might have a nightly data import task, or an ad-hoc task for optimizing images.

This page describes how you can pre-define tasks that will appear in the automatic scheduling UI. For ad-hoc background tasks, see Task manager - ad-hoc tasks (10.9.0 and above).

Screenshot of taskmanager task list

Defining tasks

The system uses a coldbox handler, Tasks.cfc, to define tasks (it also supports a ScheduledTasks.cfc handler for backward compatibility).

  • Each task is defined as a private action in the Tasks.cfc handler and decorated with metadata to give information about the task.
  • The action must return a boolean value to indicate success or failure
  • The action accepts a logger argument that should be used for all task logging - doing so will enable the live log view for your task.

For example:

// /handlers/Tasks.cfc
component {
	property name="elasticSearchEngine" inject="elasticSearchEngine";

	/**
	 * Rebuilds the search indexes from scratch, ensuring that they are all up to date with the latest data
	 *
	 * @priority         13
	 * @schedule         0 *\/15 * * * *
	 * @timeout          120
	 * @displayName      Rebuild search indexes
	 * @displayGroup     search
	 * @exclusivityGroup search
	 */
	private boolean function rebuildSearchIndexes( event, rc, prc, logger ) {
		return elasticSearchEngine.rebuildIndexes( logger=arguments.logger ?: NullValue() );
	}
}

Scheduling tasks

Tasks can be given a default schedule, or defined as not scheduled tasks using the @schedule attribute. The attribute expects a value of either disabled or an extended (6 point) cron definition in the following format:

* * * * * *
| | | | | | 
| | | | | +---- Day of the Week   (range: 1-7, 1 standing for Monday)
| | | | +------ Month of the Year (range: 1-12)
| | | +-------- Day of the Month  (range: 1-31)
| | +---------- Hour              (range: 0-23)
| +------------ Minute            (range: 0-59)
+-------------- Second            (range: 0-59)

Info

Note that there are multiple cron formats and most start with the minute definition and not seconds. However, the principal is the same in all cases. You can read more about Cron here: https://en.wikipedia.org/wiki/Cron.

Some example cron definitions:

/**
 * Every 15 minutes
 * @schedule 0 *\/15 * * * *
 *
 * At 25 minutes past the hour, every 2 hours
 * @schedule 0 25 *\/2 * * *
 *
 * At 4:06 AM, only on Tuesday
 * @schedule 0 06 04 * * 2
 */

Note how we need to escape slashes (/) in the cron syntax with a backwards slash (\). i.e. regular cron syntax: 0 */15 * * * * vs our escaped version 0 *\/15 * * * *. This is because the regular syntax would end the CFML comment with */ and render everything after that useless.

Info

The UI of the task manager also uses cron syntax for defining the schedule of tasks.

Ad-hoc tasks

You can define tasks to explicitly have no schedule, demanding that tasks are then either run programatically or manually through the admin user interface. To do so, set the @schedule attribute to disabled:

/**
 *
 * @schedule     disabled
 * @timeout      120
 * @displayName  Optimize images
 */
private boolean function optimizeImages( event, rc, prc, logger ) {
	myAwesomeImageService.doMagic( logger=argumnets.logger ?: NullValue() );
}

Task priority

When tasks run on a schedule, the system currently only allows a single task to run at any one time. If two or more tasks are due to run, the system uses the @priority value to determine which task should run first. Tasks with higher priority values will take priority over tasks with lower values.

Timeouts

Info

As of 10.10.0, timeouts are no longer supported and will be ignored. All tasks will run until they expire themselves or until 100 years, whichever comes first.

Tasks can be given a timeout value using the @timeout attribute. Values are in seconds. If the timeout is reached, the system will terminate the running thread for the task using a java thread interrupt.

Display groups

You can optionally use display groups to break-up the view of tasks in to multiple grouped tabs. For example, you may have a group for maintenance tasks and another group for CRM data syncs. Simply use the @displayGroup attribute and tasks with the same "display group" will be grouped together in tabs.

Exclusivity groups

You can optionally use exclusivity groups to ensure that related tasks do not run concurrently. For example, you may have several data syncing tasks that would be problematic if they all ran at the same time.

By default, the exclusivity group for a task is set to the display group of the task.

It you set the exclusivity group of a task to none, the task can be run at any point in time.

Use the @exclusivityGroup attribute to declare your exclusivity groups per task (or leave alone to use display group).

Info

If no groups are specified, a default group of "default" will be used.

Invoking tasks programatically

In cases where you need to start a background task as a result of some programmable event, you can call the runTask() method of the Task Manager Service directly, or use the Preside Super Class $runTask() method (see Using the super class). For example:

// /services/AssetManagerService.cfc
/**
 * @presideService
 * @singleton
 */
component {
	// ...

	public boolean function editFolderPermissions( ... ) {
		// ...

		$runTask( taskKey="moveAssets", args={ folder=arguments.folder } )

		// ...
	}

	// ...
}

Gracefully shutting down tasks

As of Preside 10.10.0, the system provides a helper method for detecting whether or not the current running thread has been "interrupted". For task manager tasks, this might happen because:

  • An admin user has hit the "Kill task button"
  • A developer has performed a framework reinit (?fwreinit=true or reload all)

When this happens, the system gives you the opportunity to detect shutdown and exit gracefully. You can do this with the $isInterrupted() method of the Preside Super Class, or by injecting the ThreadUtil service into your handler/service and calling threadUtil.isInterrupted(). For example:

/**
 * My service
 *
 * @presideservice
 * @singleton
 */
component {

	// ...
	public boolean function runSomeLongTask( logger ) {

		do {
			if ( $isInterrupted() ) {
				logger.warn( "Aborting task gracefully..." );
				break;
			}

			_doMoreWork();
		} while( _moreWorkToDo() );

		return true;
	}
}

AND/OR:

// /handlers/Tasks.cfc
component {
	property name="threadUtil" inject="threadUtil";
	property name="myService"  inject="myService";


	/**
	 * Does a load of important work
	 *
	 * @priority     13
	 * @schedule     0 *\/15 * * * *
	 * @displayName  Run things
	 * @displayGroup Stuff
	 */
	private boolean function multitask( event, rc, prc, logger ) {
		return myService.taskOne( logger ?: NullValue() )
		    && !threadUtil.isInterrupted()
		    && myService.taskTwo( logger ?: NullValue() )
		    && !threadUtil.isInterrupted()
		    && myService.taskThree( logger ?: NullValue() );
	}
}