Creating Component-Based Jobs with Quartz Scheduler
Creating Component-Based Jobs with Quartz Scheduler
This recipe provides detailed instructions for creating and configuring component-based jobs with the Quartz Scheduler extension for Lucee.
For a general overview of the Quartz Scheduler extension, see the Quartz Scheduler documentation.
Overview
Component-based jobs are a powerful feature of the Quartz Scheduler extension that allow you to execute CFML components (CFCs) as scheduled tasks. This approach provides several advantages over URL-based jobs:
- Full CFML Capabilities: Leverage the full power of CFML in your scheduled tasks
- Object-Oriented Design: Organize your scheduled tasks using proper OO principles
- Dependency Injection: Pass configuration parameters to your components
- Better Testing: Create testable, reusable components
Component Mappings
Before creating component-based jobs, it's important to understand how Lucee locates your components using component mappings.
Default Component Mappings
Every Lucee installation comes with the following default mapping configuration:
{
"componentMappings": [
{
"physical": "{lucee-config}/components/",
"virtual": "/7c0791ef8c6ceb3efef56e85a04ae393",
"archive": "",
"primary": "physical",
"inspectTemplate": "always"
}
]
}
This mapping establishes a location where Lucee looks for components, similar to classpath in Java.
Configuring Custom Mappings
You can extend the component mappings by:
-
Editing the Configuration File: Edit
lucee-server/context/.CFConfig.json
to add your own mappings -
Using the Lucee Administrator: Navigate to Server/Web Admin > Archives & Resources > Component Mappings
When a component is referenced in a Quartz Scheduler job configuration, Lucee will search for it in these configured mappings.
Creating a Component-Based Job
Step 1: Create the Component
Create a CFC with an execute()
method that contains your job logic. Optionally, include an init()
method to receive configuration parameters.
// path: {lucee-config}/components/jobs/DatabaseCleanupJob.cfc
component {
// Properties
property name="tableName" type="string";
property name="retentionDays" type="numeric";
property name="logName" type="string" default="scheduler";
// Constructor - receives job parameters
public void function init(
required string tableName,
numeric retentionDays=30,
string logName="scheduler"
) {
variables.tableName = arguments.tableName;
variables.retentionDays = arguments.retentionDays;
variables.logName = arguments.logName;
log log=variables.logName type="info" text="DatabaseCleanupJob initialized for table: #variables.tableName#";
}
// Required execute method - called when the job runs
public void function execute() {
try {
log log=variables.logName type="info" text="Starting cleanup for table: #variables.tableName#";
// Sample cleanup logic
var cutoffDate = dateAdd("d", -variables.retentionDays, now());
var result = queryExecute(
"DELETE FROM #variables.tableName# WHERE created_date < :cutoffDate",
{cutoffDate: {value: cutoffDate, cfsqltype: "CF_SQL_TIMESTAMP"}},
{datasource: "myDatasource"}
);
log log=variables.logName type="info" text="Cleanup complete. Removed #result.recordCount# records from #variables.tableName#";
}
catch(any e) {
log log=variables.logName type="error" text="Error in DatabaseCleanupJob: #e.message#" exception=e;
rethrow;
}
}
}
Step 2: Place the Component in a Mapped Location
Either:
- Save your component in the default component directory:
{lucee-config}/components/jobs/DatabaseCleanupJob.cfc
- Create a custom mapping that points to your component's location
Step 3: Configure the Job in Quartz Scheduler
Add the component job to your Quartz Scheduler configuration:
{
"jobs": [
{
"label": "Database Cleanup - User Logs",
"component": "jobs.DatabaseCleanupJob",
"cron": "0 0 3 * * ?", // Run at 3 AM daily
"pause": false,
"mode": "transient",
"tableName": "user_logs",
"retentionDays": 90
}
]
}
Component Modes
Quartz Scheduler supports two modes for component jobs:
-
Transient Mode (default):
- Creates a new instance of the component for each execution
- Useful for jobs that don't need to maintain state between executions
- Configuration:
"mode": "transient"
-
Singleton Mode:
- Creates a single instance that's reused across all executions
- Useful for jobs that maintain state or have expensive initialization
- Configuration:
"mode": "singleton"
Example of singleton mode:
{
"label": "Incremental Data Processor",
"component": "jobs.DataProcessor",
"cron": "0 */15 * * * ?", // Every 15 minutes
"mode": "singleton",
"batchSize": 100
}
Creating a Job Listener
Job listeners allow you to monitor and respond to job execution events. They can be used for logging, notifications, or to implement more complex job coordination.
Step 1: Create the Listener Component
Create a CFC that implements the necessary listener methods:
// path: {lucee-config}/components/listeners/JobMonitorListener.cfc
component {
// Properties
property name="name" type="string";
property name="stream" type="string";
property name="logFile" type="string";
// Constructor - receives listener parameters
public void function init(struct listenerData) {
variables.name = "JobMonitorListener";
variables.stream = listenerData.stream ?: "err";
variables.logFile = listenerData.logFile ?: "";
// Initialize any resources
if (len(variables.logFile)) {
// Ensure log directory exists
var logDir = getDirectoryFromPath(variables.logFile);
if (!directoryExists(logDir)) {
directoryCreate(logDir);
}
}
}
// Required method - returns the name of the listener
public string function getName() {
return variables.name;
}
// Called before a job executes
public void function jobToBeExecuted(jobExecutionContext) {
var jobDetail = jobExecutionContext.getJobDetail();
var jobDataMap = jobDetail.getJobDataMap();
var jobName = jobDataMap.get("label") ?: jobDetail.getKey().toString();
var message = "#now()# - Job starting: #jobName#";
writeToLog(message);
}
// Called after a job executes
public void function jobWasExecuted(jobExecutionContext, jobException) {
var jobDetail = jobExecutionContext.getJobDetail();
var jobDataMap = jobDetail.getJobDataMap();
var jobName = jobDataMap.get("label") ?: jobDetail.getKey().toString();
if (isNull(jobException)) {
var message = "#now()# - Job completed successfully: #jobName#";
} else {
var message = "#now()# - Job failed: #jobName# - Error: #jobException.getMessage()#";
}
writeToLog(message);
}
// Called when a job is vetoed
public void function jobExecutionVetoed(jobExecutionContext) {
var jobDetail = jobExecutionContext.getJobDetail();
var jobDataMap = jobDetail.getJobDataMap();
var jobName = jobDataMap.get("label") ?: jobDetail.getKey().toString();
var message = "#now()# - Job execution vetoed: #jobName#";
writeToLog(message);
}
// Helper function to write to log
private void function writeToLog(required string message) {
// Write to console
if (variables.stream == "out") {
systemOutput(message, true, true);
} else {
systemOutput(message, true, false);
}
// Write to log file if configured
if (len(variables.logFile)) {
fileAppend(variables.logFile, message & chr(13) & chr(10));
}
}
}
Step 2: Place the Listener in a Mapped Location
Save your listener component in a location accessible via component mapping, such as:
{lucee-config}/components/listeners/JobMonitorListener.cfc
Step 3: Configure the Listener in Quartz Scheduler
Add the listener to your Quartz Scheduler configuration:
{
"listeners": [
{
"component": "listeners.JobMonitorListener",
"stream": "err",
"logFile": "{lucee-config}/logs/quartz-jobs.log"
}
]
}
Best Practices
-
Organize Your Components:
- Create a clear structure for your job components (e.g., by function or application area)
- Use namespaces to avoid conflicts (e.g.,
myapp.jobs.DataCleanup
)
-
Handle Exceptions Properly:
- Always implement error handling in your
execute()
method - Log detailed error information to help with troubleshooting
- Always implement error handling in your
-
Keep Jobs Focused:
- Each job component should have a single responsibility
- For complex operations, consider creating helper components
-
Use Dependency Injection:
- Pass configuration values through the job configuration
- Avoid hardcoding values in your components
-
Include Logging:
- Add detailed logging to track job execution
- Use listeners for centralized monitoring
Conclusion
Component-based jobs in Quartz Scheduler provide a powerful way to organize and implement your scheduled tasks in Lucee. By understanding component mappings and following these patterns, you can create maintainable, testable job components that leverage the full power of CFML.
Remember to place your components in locations accessible via component mappings and to configure your jobs properly in the Quartz Scheduler configuration.