|
| 1 | +<!-- |
| 2 | +{ |
| 3 | + "title": "Creating Component-Based Jobs with Quartz Scheduler", |
| 4 | + "id": "component-jobs-quartz-scheduler", |
| 5 | + "related": ["scheduler-quartz", "clustering-quartz-scheduler"], |
| 6 | + "categories": ["extensions", "recipes"], |
| 7 | + "description": "How to create and configure component-based jobs with the Quartz Scheduler extension", |
| 8 | + "keywords": [ |
| 9 | + "scheduler", |
| 10 | + "quartz", |
| 11 | + "component", |
| 12 | + "cfc", |
| 13 | + "jobs", |
| 14 | + "listeners" |
| 15 | + ] |
| 16 | +} |
| 17 | +--> |
| 18 | + |
| 19 | +# Creating Component-Based Jobs with Quartz Scheduler |
| 20 | + |
| 21 | +This recipe provides detailed instructions for creating and configuring component-based jobs with the Quartz Scheduler extension for Lucee. |
| 22 | + |
| 23 | +For a general overview of the Quartz Scheduler extension, see the [Quartz Scheduler documentation](https://github.com/lucee/lucee-docs/blob/master/docs/recipes/scheduler-quartz.md). |
| 24 | + |
| 25 | +## Overview |
| 26 | + |
| 27 | +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: |
| 28 | + |
| 29 | +- **Full CFML Capabilities**: Leverage the full power of CFML in your scheduled tasks |
| 30 | +- **Object-Oriented Design**: Organize your scheduled tasks using proper OO principles |
| 31 | +- **Dependency Injection**: Pass configuration parameters to your components |
| 32 | +- **Better Testing**: Create testable, reusable components |
| 33 | + |
| 34 | +## Component Mappings |
| 35 | + |
| 36 | +Before creating component-based jobs, it's important to understand how Lucee locates your components using component mappings. |
| 37 | + |
| 38 | +### Default Component Mappings |
| 39 | + |
| 40 | +Every Lucee installation comes with the following default mapping configuration: |
| 41 | + |
| 42 | +```json |
| 43 | +{ |
| 44 | + "componentMappings": [ |
| 45 | + { |
| 46 | + "physical": "{lucee-config}/components/", |
| 47 | + "virtual": "/7c0791ef8c6ceb3efef56e85a04ae393", |
| 48 | + "archive": "", |
| 49 | + "primary": "physical", |
| 50 | + "inspectTemplate": "always" |
| 51 | + } |
| 52 | + ] |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +This mapping establishes a location where Lucee looks for components, similar to classpath in Java. |
| 57 | + |
| 58 | +### Configuring Custom Mappings |
| 59 | + |
| 60 | +You can extend the component mappings by: |
| 61 | + |
| 62 | +1. **Editing the Configuration File**: |
| 63 | + Edit `lucee-server/context/.CFConfig.json` to add your own mappings |
| 64 | + |
| 65 | +2. **Using the Lucee Administrator**: |
| 66 | + Navigate to Server/Web Admin > Archives & Resources > Component Mappings |
| 67 | + |
| 68 | +When a component is referenced in a Quartz Scheduler job configuration, Lucee will search for it in these configured mappings. |
| 69 | + |
| 70 | +## Creating a Component-Based Job |
| 71 | + |
| 72 | +### Step 1: Create the Component |
| 73 | + |
| 74 | +Create a CFC with an `execute()` method that contains your job logic. Optionally, include an `init()` method to receive configuration parameters. |
| 75 | + |
| 76 | +```cfml |
| 77 | +// path: {lucee-config}/components/jobs/DatabaseCleanupJob.cfc |
| 78 | +component { |
| 79 | + |
| 80 | + // Properties |
| 81 | + property name="tableName" type="string"; |
| 82 | + property name="retentionDays" type="numeric"; |
| 83 | + property name="logName" type="string" default="scheduler"; |
| 84 | + |
| 85 | + // Constructor - receives job parameters |
| 86 | + public void function init( |
| 87 | + required string tableName, |
| 88 | + numeric retentionDays=30, |
| 89 | + string logName="scheduler" |
| 90 | + ) { |
| 91 | + variables.tableName = arguments.tableName; |
| 92 | + variables.retentionDays = arguments.retentionDays; |
| 93 | + variables.logName = arguments.logName; |
| 94 | + |
| 95 | + log log=variables.logName type="info" text="DatabaseCleanupJob initialized for table: #variables.tableName#"; |
| 96 | + } |
| 97 | + |
| 98 | + // Required execute method - called when the job runs |
| 99 | + public void function execute() { |
| 100 | + try { |
| 101 | + log log=variables.logName type="info" text="Starting cleanup for table: #variables.tableName#"; |
| 102 | + |
| 103 | + // Sample cleanup logic |
| 104 | + var cutoffDate = dateAdd("d", -variables.retentionDays, now()); |
| 105 | + var result = queryExecute( |
| 106 | + "DELETE FROM #variables.tableName# WHERE created_date < :cutoffDate", |
| 107 | + {cutoffDate: {value: cutoffDate, cfsqltype: "CF_SQL_TIMESTAMP"}}, |
| 108 | + {datasource: "myDatasource"} |
| 109 | + ); |
| 110 | + |
| 111 | + log log=variables.logName type="info" text="Cleanup complete. Removed #result.recordCount# records from #variables.tableName#"; |
| 112 | + } |
| 113 | + catch(any e) { |
| 114 | + log log=variables.logName type="error" text="Error in DatabaseCleanupJob: #e.message#" exception=e; |
| 115 | + rethrow; |
| 116 | + } |
| 117 | + } |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +### Step 2: Place the Component in a Mapped Location |
| 122 | + |
| 123 | +Either: |
| 124 | +1. Save your component in the default component directory: `{lucee-config}/components/jobs/DatabaseCleanupJob.cfc` |
| 125 | +2. Create a custom mapping that points to your component's location |
| 126 | + |
| 127 | +### Step 3: Configure the Job in Quartz Scheduler |
| 128 | + |
| 129 | +Add the component job to your Quartz Scheduler configuration: |
| 130 | + |
| 131 | +```json |
| 132 | +{ |
| 133 | + "jobs": [ |
| 134 | + { |
| 135 | + "label": "Database Cleanup - User Logs", |
| 136 | + "component": "jobs.DatabaseCleanupJob", |
| 137 | + "cron": "0 0 3 * * ?", // Run at 3 AM daily |
| 138 | + "pause": false, |
| 139 | + "mode": "transient", |
| 140 | + "tableName": "user_logs", |
| 141 | + "retentionDays": 90 |
| 142 | + } |
| 143 | + ] |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +## Component Modes |
| 148 | + |
| 149 | +Quartz Scheduler supports two modes for component jobs: |
| 150 | + |
| 151 | +1. **Transient Mode** (default): |
| 152 | + - Creates a new instance of the component for each execution |
| 153 | + - Useful for jobs that don't need to maintain state between executions |
| 154 | + - Configuration: `"mode": "transient"` |
| 155 | + |
| 156 | +2. **Singleton Mode**: |
| 157 | + - Creates a single instance that's reused across all executions |
| 158 | + - Useful for jobs that maintain state or have expensive initialization |
| 159 | + - Configuration: `"mode": "singleton"` |
| 160 | + |
| 161 | +Example of singleton mode: |
| 162 | + |
| 163 | +```json |
| 164 | +{ |
| 165 | + "label": "Incremental Data Processor", |
| 166 | + "component": "jobs.DataProcessor", |
| 167 | + "cron": "0 */15 * * * ?", // Every 15 minutes |
| 168 | + "mode": "singleton", |
| 169 | + "batchSize": 100 |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +## Creating a Job Listener |
| 174 | + |
| 175 | +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. |
| 176 | + |
| 177 | +### Step 1: Create the Listener Component |
| 178 | + |
| 179 | +Create a CFC that implements the necessary listener methods: |
| 180 | + |
| 181 | +```cfml |
| 182 | +// path: {lucee-config}/components/listeners/JobMonitorListener.cfc |
| 183 | +component { |
| 184 | + |
| 185 | + // Properties |
| 186 | + property name="name" type="string"; |
| 187 | + property name="stream" type="string"; |
| 188 | + property name="logFile" type="string"; |
| 189 | + |
| 190 | + // Constructor - receives listener parameters |
| 191 | + public void function init(struct listenerData) { |
| 192 | + variables.name = "JobMonitorListener"; |
| 193 | + variables.stream = listenerData.stream ?: "err"; |
| 194 | + variables.logFile = listenerData.logFile ?: ""; |
| 195 | + |
| 196 | + // Initialize any resources |
| 197 | + if (len(variables.logFile)) { |
| 198 | + // Ensure log directory exists |
| 199 | + var logDir = getDirectoryFromPath(variables.logFile); |
| 200 | + if (!directoryExists(logDir)) { |
| 201 | + directoryCreate(logDir); |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + // Required method - returns the name of the listener |
| 207 | + public string function getName() { |
| 208 | + return variables.name; |
| 209 | + } |
| 210 | + |
| 211 | + // Called before a job executes |
| 212 | + public void function jobToBeExecuted(jobExecutionContext) { |
| 213 | + var jobDetail = jobExecutionContext.getJobDetail(); |
| 214 | + var jobDataMap = jobDetail.getJobDataMap(); |
| 215 | + var jobName = jobDataMap.get("label") ?: jobDetail.getKey().toString(); |
| 216 | + |
| 217 | + var message = "#now()# - Job starting: #jobName#"; |
| 218 | + writeToLog(message); |
| 219 | + } |
| 220 | + |
| 221 | + // Called after a job executes |
| 222 | + public void function jobWasExecuted(jobExecutionContext, jobException) { |
| 223 | + var jobDetail = jobExecutionContext.getJobDetail(); |
| 224 | + var jobDataMap = jobDetail.getJobDataMap(); |
| 225 | + var jobName = jobDataMap.get("label") ?: jobDetail.getKey().toString(); |
| 226 | + |
| 227 | + if (isNull(jobException)) { |
| 228 | + var message = "#now()# - Job completed successfully: #jobName#"; |
| 229 | + } else { |
| 230 | + var message = "#now()# - Job failed: #jobName# - Error: #jobException.getMessage()#"; |
| 231 | + } |
| 232 | + |
| 233 | + writeToLog(message); |
| 234 | + } |
| 235 | + |
| 236 | + // Called when a job is vetoed |
| 237 | + public void function jobExecutionVetoed(jobExecutionContext) { |
| 238 | + var jobDetail = jobExecutionContext.getJobDetail(); |
| 239 | + var jobDataMap = jobDetail.getJobDataMap(); |
| 240 | + var jobName = jobDataMap.get("label") ?: jobDetail.getKey().toString(); |
| 241 | + |
| 242 | + var message = "#now()# - Job execution vetoed: #jobName#"; |
| 243 | + writeToLog(message); |
| 244 | + } |
| 245 | + |
| 246 | + // Helper function to write to log |
| 247 | + private void function writeToLog(required string message) { |
| 248 | + // Write to console |
| 249 | + if (variables.stream == "out") { |
| 250 | + systemOutput(message, true, true); |
| 251 | + } else { |
| 252 | + systemOutput(message, true, false); |
| 253 | + } |
| 254 | + |
| 255 | + // Write to log file if configured |
| 256 | + if (len(variables.logFile)) { |
| 257 | + fileAppend(variables.logFile, message & chr(13) & chr(10)); |
| 258 | + } |
| 259 | + } |
| 260 | +} |
| 261 | +``` |
| 262 | + |
| 263 | +### Step 2: Place the Listener in a Mapped Location |
| 264 | + |
| 265 | +Save your listener component in a location accessible via component mapping, such as: |
| 266 | +`{lucee-config}/components/listeners/JobMonitorListener.cfc` |
| 267 | + |
| 268 | +### Step 3: Configure the Listener in Quartz Scheduler |
| 269 | + |
| 270 | +Add the listener to your Quartz Scheduler configuration: |
| 271 | + |
| 272 | +```json |
| 273 | +{ |
| 274 | + "listeners": [ |
| 275 | + { |
| 276 | + "component": "listeners.JobMonitorListener", |
| 277 | + "stream": "err", |
| 278 | + "logFile": "{lucee-config}/logs/quartz-jobs.log" |
| 279 | + } |
| 280 | + ] |
| 281 | +} |
| 282 | +``` |
| 283 | + |
| 284 | +## Best Practices |
| 285 | + |
| 286 | +1. **Organize Your Components**: |
| 287 | + - Create a clear structure for your job components (e.g., by function or application area) |
| 288 | + - Use namespaces to avoid conflicts (e.g., `myapp.jobs.DataCleanup`) |
| 289 | + |
| 290 | +2. **Handle Exceptions Properly**: |
| 291 | + - Always implement error handling in your `execute()` method |
| 292 | + - Log detailed error information to help with troubleshooting |
| 293 | + |
| 294 | +3. **Keep Jobs Focused**: |
| 295 | + - Each job component should have a single responsibility |
| 296 | + - For complex operations, consider creating helper components |
| 297 | + |
| 298 | +4. **Use Dependency Injection**: |
| 299 | + - Pass configuration values through the job configuration |
| 300 | + - Avoid hardcoding values in your components |
| 301 | + |
| 302 | +5. **Include Logging**: |
| 303 | + - Add detailed logging to track job execution |
| 304 | + - Use listeners for centralized monitoring |
| 305 | + |
| 306 | +## Conclusion |
| 307 | + |
| 308 | +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. |
| 309 | + |
| 310 | +Remember to place your components in locations accessible via component mappings and to configure your jobs properly in the Quartz Scheduler configuration. |
0 commit comments