Java Quartz Best Practices Example
1. Introduction
In this post, we are going to demonstrate best practices for Quartz, a very popular open source job scheduling library that can be used in Java applications. Most of the best practices that we will see have been published by Quartz in this article.
The main concept of Quartz is that a scheduler holds a list of jobs that are triggered at specific times or repeatedly. The basic components of Quartz that we will focus on are:
- Job – Represents the actual job to be executed
- JobDetail – Conveys the detail properties of a given Job instance
- Trigger – Triggers are the mechanism by which Jobs are scheduled
- JobListener – Called by the scheduler before and after a Job gets executed
Table Of Contents
- 1. Introduction
- 2. Project Setup
- 3. Quartz Best Practices
- 3.1 Make use of Builders
- 3.2 Store Primitives or String in JobDataMap
- 3.3 Use the MergedJobDataMap
- 3.4 Handle Exceptions inside Jobs
- 3.5 Disallow Concurrent Job Execution
- 3.6 Idempotent Job Execution
- 3.7 Job Listeners Execution
- 3.8 Securing the Scheduler
- 3.9 Skip Update Check
- 3.10 Avoid scheduling Jobs at DST
- 3.11 Enable detailed Logging
- 4. Conclusion
- 5. Download the Eclipse project
2. Project Setup
To run the code examples of this post, we will use the following technologies:
- Java 8
- Quartz 2.2.1
- SLF4J 1.7.26
- Logback 1.2.3
- Maven 3.3.3
- Eclipse 4.10.0
Maven will be used as the tool to build the project so let’s add the Quartz, SLF4J and logback libraries as dependencies to the pom.xml:
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.javacodegeeks</groupId> <artifactId>quartz-best-practices</artifactId> <version>0.0.1-SNAPSHOT</version> <description>Java Quartz Best Practices Example</description> <properties> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.source>1.8</maven.compiler.source> </properties> <dependencies> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.26</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> </dependency> </dependencies> </project>
3. Quartz Best Practices
3.1 Make use of Builders
To simplify the creation of JobDetail
and Trigger
instances, Quartz provides the Builder classes org.quartz.JobBuilder
and org.quartz.TriggerBuilder
respectively. The Builder pattern is a very common design pattern which provides a flexible solution to various object creation problems in object-oriented programming.
Below we can see a running example of a Quartz application which uses those builder classes to create instances in an easy way.
QuartzBuildersExample.java
package com.javacodegeeks.quartz.builders; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.SimpleScheduleBuilder; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.impl.StdSchedulerFactory; public class QuartzBuildersExample { public void run() throws Exception { // create the scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // define the job and tie it to the SimpleJob class JobDetail job = JobBuilder.newJob(SimpleJob.class) .withIdentity("myJob", "myGroup") .build(); // create the trigger and define its schedule to run every 3 seconds Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("myTrigger", "myGroup") .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(3) .repeatForever()) .build(); // add the job details to the scheduler and associate it with the trigger scheduler.scheduleJob(job, trigger); // start the scheduler scheduler.start(); // wait long enough to see the job execution Thread.sleep(8 * 1000); // shutdown the scheduler scheduler.shutdown(true); } public static void main(String[] args) throws Exception { // run the example QuartzBuildersExample example = new QuartzBuildersExample(); example.run(); } }
SimpleJob.java
package com.javacodegeeks.quartz.builders; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SimpleJob implements Job { private final Logger log = LoggerFactory.getLogger(SimpleJob.class); public void execute(JobExecutionContext context) throws JobExecutionException { log.info("SimpleJob executed!"); } }
In the example above, we create the SimpleJob
job, a very simple job which only outputs a single line of code when it gets executed. In the QuartzBuildersExample
class, this job is added to the scheduler and set for execution in the JobDetail
instance, which is created using the JobBuilder
class. The job is associated with the Trigger
instance created using the TriggerBuilder
class. During the creation of the trigger another builder class, the SimpleScheduleBuilder
class, creates a SchedulerBuilder
which defines the trigger interval schedule every 3 seconds. Finally the scheduler shuts down after 8 seconds (see Thread.sleep(8 * 1000)
). Let’s run main method of QuartzBuildersExample
class to see the output:
Output
09:52:33.179 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor 09:52:33.204 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 09:52:33.204 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.2.1 created. 09:52:33.206 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized. 09:52:33.207 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.1) 'QuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 09:52:33.207 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'QuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties' 09:52:33.207 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.2.1 09:52:33.214 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED started. 09:52:33.215 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 09:52:33.222 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.javacodegeeks.quartz.builders.SimpleJob 09:52:33.227 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 09:52:33.227 [QuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob 09:52:33.227 [QuartzScheduler_Worker-1] INFO com.javacodegeeks.quartz.builders.SimpleJob - SimpleJob executed! 09:52:36.213 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.javacodegeeks.quartz.builders.SimpleJob 09:52:36.213 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 09:52:36.213 [QuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob 09:52:36.213 [QuartzScheduler_Worker-2] INFO com.javacodegeeks.quartz.builders.SimpleJob - SimpleJob executed! 09:52:39.213 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.javacodegeeks.quartz.builders.SimpleJob 09:52:39.213 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 09:52:39.213 [QuartzScheduler_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob 09:52:39.213 [QuartzScheduler_Worker-3] INFO com.javacodegeeks.quartz.builders.SimpleJob - SimpleJob executed! 09:52:41.218 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutting down. 09:52:41.218 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED paused. 09:52:41.219 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutting down threadpool... 09:52:41.242 [QuartzScheduler_Worker-2] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 09:52:41.255 [QuartzScheduler_Worker-1] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 09:52:41.721 [QuartzScheduler_Worker-3] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 09:52:41.722 [main] DEBUG org.quartz.simpl.SimpleThreadPool - No executing jobs remaining, all threads stopped. 09:52:41.722 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutdown of threadpool complete. 09:52:41.722 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutdown complete.
From the above output, we see that the job ran 3 times in total, at an interval of 3 seconds, before the scheduler was manually shut down by the main thread.
3.2 Store Primitives or String in JobDataMap
In order to hold state information for Job
instances, we can pass a JobDataMap
upon creation of a JobDetail
or a Trigger
. The JobDataMap
class is a key-value pair data type which implements the Map
interface. To avoid data serialization issues, we should store either primitive data types or Strings in a JobDataMap
.
In the QuartzJobDataMapExample
class below we show how to pass job data maps to a job.
QuartzJobDataMapExample.java
package com.javacodegeeks.quartz.jobdatamap; import org.quartz.JobBuilder; import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.impl.StdSchedulerFactory; public class QuartzJobDataMapExample { public void run() throws Exception { SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // create a job data map and pass the job name and version JobDataMap jobDetailDataMap = new JobDataMap(); jobDetailDataMap.put("jobName", "MyCustomJob"); jobDetailDataMap.put("jobVersion", 1); // pass the jobDetailDataMap when creating the job detail JobDetail job = JobBuilder.newJob(DataMapJob.class) .withIdentity("MyCustomJob", "group") .usingJobData(jobDetailDataMap) .build(); // create a job data map and pass the trigger name JobDataMap triggerJobDataMap = new JobDataMap(); triggerJobDataMap.put("triggerName", "MyCustomTrigger"); // pass the triggerJobDataMap when creating the trigger Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("MyCustomTrigger", "group") .usingJobData(triggerJobDataMap) .build(); scheduler.scheduleJob(job, trigger); scheduler.start(); // wait long enough to see the job execution Thread.sleep(1 * 1000); scheduler.shutdown(true); } public static void main(String[] args) throws Exception { QuartzJobDataMapExample example = new QuartzJobDataMapExample(); example.run(); } }
In the above example, we create two job data maps. The first is created in lines 19-21 and passes the jobName and jobVersion during the creation of the job detail, whereas the second is created in lines 30-31 and passes the triggerName during the creation of the trigger. As we can see we only store Strings (jobName, triggerName) and primitives (jobVersion) in the job data maps.
We will take a look at the DataMapJob
job and the output in the following section.
3.3 Use the MergedJobDataMap
As we saw from the previous section a job data map can be passed either to a job detail or a trigger. In order to properly retrieve all the values from the job data maps, we should use the JobExecutionContext.getMergedJobDataMap()
method inside the execute
method of a job and not the JobExecutionContext.getJobDetail().getJobDataMap()
, as the latter will only return the values from the job data map passed to the job details and not the trigger.
Let’s see in the following example how the DataMapJob
job retrieves the values from the two job data maps we created in the previous example.
DataMapJob.java
package com.javacodegeeks.quartz.jobdatamap; import org.quartz.Job; import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DataMapJob implements Job { private final Logger log = LoggerFactory.getLogger(DataMapJob.class); public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap dataMap = context.getMergedJobDataMap(); String jobName = dataMap.getString("jobName"); int jobVersion = dataMap.getInt("jobVersion"); String triggerName = dataMap.getString("triggerName"); log.info("The name and version of job is {} {}, triggered by {}!", jobName, jobVersion, triggerName); } }
As we see from the code above, we retrieve the values of the two job data maps we passed to the job detail and trigger, using the JobExecutionContext.getMergedJobDataMap()
method inside the DataMapJob
job.
Let’s run the main method of QuartzJobDataMapExample
class we created in the previous example and confirm that the output will print all the values we passed to the job from both job data maps.
Output
10:46:42.359 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor 10:46:42.384 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 10:46:42.384 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.2.1 created. 10:46:42.385 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized. 10:46:42.386 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.1) 'QuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 10:46:42.387 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'QuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties' 10:46:42.387 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.2.1 10:46:42.400 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED started. 10:46:42.402 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 10:46:42.404 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group.MyCustomJob', class=com.javacodegeeks.quartz.jobdatamap.DataMapJob 10:46:42.407 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers 10:46:42.407 [QuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group.MyCustomJob 10:46:42.407 [QuartzScheduler_Worker-1] INFO com.javacodegeeks.quartz.jobdatamap.DataMapJob - The name and version of job is MyCustomJob 1, triggered by MyCustomTrigger! 10:46:43.405 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutting down. 10:46:43.406 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED paused. 10:46:43.406 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutting down threadpool... 10:46:43.418 [QuartzScheduler_Worker-1] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 10:46:43.880 [QuartzScheduler_Worker-2] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 10:46:43.880 [QuartzScheduler_Worker-3] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 10:46:43.880 [main] DEBUG org.quartz.simpl.SimpleThreadPool - No executing jobs remaining, all threads stopped. 10:46:43.880 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutdown of threadpool complete. 10:46:43.880 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutdown complete.
From the output above, in line 20 we see that the values from both job data maps are printed correctly.
3.4 Handle Exceptions inside Jobs
The code that gets executed within a job should contain a try-catch block and be able to retry executing itself, for a maximum number of retries. Otherwise, if a job throws an exception without catching it, Quartz will try to re-execute the job and most probably fail again which might lead to an infinite loop.
Find below a job which handles its exceptions and re-tries its execution.
QuartzExceptionHandlingExample.java
package com.javacodegeeks.quartz.exceptionhandling; import org.quartz.JobBuilder; import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.impl.StdSchedulerFactory; public class QuartzExceptionHandlingExample { public void run() throws Exception { SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put("retries", 3); JobDetail job = JobBuilder.newJob(ExceptionHandlingJob.class) .withIdentity("job", "group") .usingJobData(jobDataMap) .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger", "group") .build(); scheduler.scheduleJob(job, trigger); scheduler.start(); // wait long enough to see the job execution Thread.sleep(5 * 1000); scheduler.shutdown(true); } public static void main(String[] args) throws Exception { QuartzExceptionHandlingExample example = new QuartzExceptionHandlingExample(); example.run(); } }
ExceptionHandlingJob.java
package com.javacodegeeks.quartz.exceptionhandling; import org.quartz.Job; import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ExceptionHandlingJob implements Job { private final Logger log = LoggerFactory.getLogger(ExceptionHandlingJob.class); public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap dataMap = context.getMergedJobDataMap(); int retries = dataMap.getInt("retries"); try { // exception will be thrown - don't do that! int result = 10 / 0; log.info("ExceptionHandlingJob never reaches this line"); } catch (ArithmeticException e) { log.error("Exception occured during execution of ExceptionHandlingJob, retry {} more time(s)", retries); // decrease the number of retries of the job data map dataMap.put("retries", --retries); JobExecutionException e2 = new JobExecutionException(e); // this job will refire immediately if (retries > 0) { e2.setRefireImmediately(true); throw e2; } } } }
In the example above, the number of retries is passed to a job data map, exactly as we saw in previous examples. For the purpose of this example, the ExceptionHandlingJob
job throws an ArithmeticException
as there is division by zero (line 20). The exception is caught by the job which tries to re-execute itself for 2 more times (line 31), by decreasing the number of retries in the job data map passed (line 26). After 3 total failed executions, the job stops running and the trigger is then responsible for re-scheduling this job.
Let’s run the application and check out the output.
Output
10:56:18.883 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor 10:56:18.915 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 10:56:18.915 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.2.1 created. 10:56:18.917 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized. 10:56:18.918 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.1) 'QuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 10:56:18.919 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'QuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties' 10:56:18.919 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.2.1 10:56:18.940 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED started. 10:56:18.942 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 10:56:18.945 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group.job', class=com.javacodegeeks.quartz.exceptionhandling.ExceptionHandlingJob 10:56:18.949 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers 10:56:18.949 [QuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group.job 10:56:18.949 [QuartzScheduler_Worker-1] ERROR com.javacodegeeks.quartz.exceptionhandling.ExceptionHandlingJob - Exception occured during execution of ExceptionHandlingJob, retry 3 more time(s) 10:56:18.956 [QuartzScheduler_Worker-1] INFO org.quartz.core.JobRunShell - Job group.job threw a JobExecutionException: org.quartz.JobExecutionException: java.lang.ArithmeticException: / by zero at com.javacodegeeks.quartz.exceptionhandling.ExceptionHandlingJob.execute(ExceptionHandlingJob.java:27) at org.quartz.core.JobRunShell.run(JobRunShell.java:202) at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) Caused by: java.lang.ArithmeticException: / by zero at com.javacodegeeks.quartz.exceptionhandling.ExceptionHandlingJob.execute(ExceptionHandlingJob.java:20) ... 2 common frames omitted 10:56:18.957 [QuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group.job 10:56:18.957 [QuartzScheduler_Worker-1] ERROR com.javacodegeeks.quartz.exceptionhandling.ExceptionHandlingJob - Exception occured during execution of ExceptionHandlingJob, retry 2 more time(s) 10:56:18.957 [QuartzScheduler_Worker-1] INFO org.quartz.core.JobRunShell - Job group.job threw a JobExecutionException: org.quartz.JobExecutionException: java.lang.ArithmeticException: / by zero at com.javacodegeeks.quartz.exceptionhandling.ExceptionHandlingJob.execute(ExceptionHandlingJob.java:27) at org.quartz.core.JobRunShell.run(JobRunShell.java:202) at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) Caused by: java.lang.ArithmeticException: / by zero at com.javacodegeeks.quartz.exceptionhandling.ExceptionHandlingJob.execute(ExceptionHandlingJob.java:20) ... 2 common frames omitted 10:56:18.957 [QuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group.job 10:56:18.957 [QuartzScheduler_Worker-1] ERROR com.javacodegeeks.quartz.exceptionhandling.ExceptionHandlingJob - Exception occured during execution of ExceptionHandlingJob, retry 1 more time(s) 10:56:23.943 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutting down. 10:56:23.943 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED paused. 10:56:23.943 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutting down threadpool... 10:56:23.980 [QuartzScheduler_Worker-1] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 10:56:24.415 [QuartzScheduler_Worker-2] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 10:56:24.416 [QuartzScheduler_Worker-3] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 10:56:24.416 [main] DEBUG org.quartz.simpl.SimpleThreadPool - No executing jobs remaining, all threads stopped. 10:56:24.416 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutdown of threadpool complete. 10:56:24.416 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutdown complete.
From the above output, the job did run 3 failed times and then terminated its execution.
3.5 Disallow Concurrent Job Execution
In most use cases, we would want to disallow the execution of more than one instances of the same job at the same time, to prevent race conditions on saved data. This might occur when the jobs take too long to finish or are triggered too often. To achieve that we have to use the annotation @DisallowConcurrentExecution
on the Job class. This is demonstrated in the example below.
QuartzDisallowConcurrentExecutionExample.java
package com.javacodegeeks.quartz.disallowconcurrentexecution; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.SimpleScheduleBuilder; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.impl.StdSchedulerFactory; public class QuartzDisallowConcurrentExecutionExample { void run() throws Exception { SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); JobDetail job = JobBuilder.newJob(DisallowConcurrentExecutionJob.class) .withIdentity("job", "group") .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger", "group") .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(3) .repeatForever()) .build(); scheduler.scheduleJob(job, trigger); scheduler.start(); // wait long enough to see the job execution Thread.sleep(60 * 1000); scheduler.shutdown(true); } public static void main(String[] args) throws Exception { QuartzDisallowConcurrentExecutionExample example = new QuartzDisallowConcurrentExecutionExample(); example.run(); } }
DisallowConcurrentExecutionJob.java
package com.javacodegeeks.quartz.disallowconcurrentexecution; import java.util.Date; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @DisallowConcurrentExecution public class DisallowConcurrentExecutionJob implements Job { private final Logger log = LoggerFactory.getLogger(DisallowConcurrentExecutionJob.class); public void execute(JobExecutionContext context) throws JobExecutionException { try { log.info("DisallowConcurrentExecutionJob executed on {}", new Date()); Thread.sleep(5000); // Don't do that! It's for the sake of the exercise.. } catch (InterruptedException e) { e.printStackTrace(); } } }
The code above schedules the same job every 3 seconds and each job takes 5 seconds to finish. Having added the @DisallowConcurrentExecution
annotation on the job, there would be only one job instance running at the same time, so each job will run every 5 seconds and not every 3 seconds as it has been scheduled. If we run the application the output would be:
Output
11:12:57.137 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor 11:12:57.159 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 11:12:57.160 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.2.1 created. 11:12:57.160 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized. 11:12:57.161 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.1) 'QuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 11:12:57.161 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'QuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties' 11:12:57.161 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.2.1 11:12:57.167 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED started. 11:12:57.176 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 11:12:57.183 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group.job', class=com.javacodegeeks.quartz.disallowconcurrentexecution.DisallowConcurrentExecutionJob 11:12:57.185 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers 11:12:57.186 [QuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group.job 11:12:57.186 [QuartzScheduler_Worker-1] INFO com.javacodegeeks.quartz.disallowconcurrentexecution.DisallowConcurrentExecutionJob - DisallowConcurrentExecutionJob executed on Mon Mar 04 09:12:57 EET 2019 11:13:02.195 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 11:13:02.195 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group.job', class=com.javacodegeeks.quartz.disallowconcurrentexecution.DisallowConcurrentExecutionJob 11:13:02.195 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers 11:13:02.196 [QuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job group.job 11:13:02.196 [QuartzScheduler_Worker-2] INFO com.javacodegeeks.quartz.disallowconcurrentexecution.DisallowConcurrentExecutionJob - DisallowConcurrentExecutionJob executed on Mon Mar 04 09:13:02 EET 2019 11:13:07.200 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 11:13:07.201 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group.job', class=com.javacodegeeks.quartz.disallowconcurrentexecution.DisallowConcurrentExecutionJob 11:13:07.201 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers 11:13:07.201 [QuartzScheduler_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job group.job 11:13:07.201 [QuartzScheduler_Worker-3] INFO com.javacodegeeks.quartz.disallowconcurrentexecution.DisallowConcurrentExecutionJob - DisallowConcurrentExecutionJob executed on Mon Mar 04 09:13:07 EET 2019 11:13:12.171 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutting down. 11:13:12.171 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED paused. 11:13:12.172 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutting down threadpool... 11:13:12.172 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Waiting for thread QuartzScheduler_Worker-3 to shut down 11:13:12.205 [QuartzScheduler_Worker-3] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 11:13:12.229 [QuartzScheduler_Worker-2] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 11:13:12.245 [QuartzScheduler_Worker-1] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 11:13:12.246 [main] DEBUG org.quartz.simpl.SimpleThreadPool - No executing jobs remaining, all threads stopped. 11:13:12.246 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutdown of threadpool complete. 11:13:12.246 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutdown complete.
The above output confirms that each job instance ran every 5 seconds and not every 3 seconds, as we disallowed concurrent executions.
3.6 Idempotent Job Execution
As already discussed in previous examples, a job that fails to be executed successfully might be subject to re-executing itself. If only a code segment runs and not the whole execution of the job, then we might end up in wrong results or even redundant data. To resolve that, the job execution should be idempotent. That means that if a job runs multiple times (after failures) it must always produce the same result.
3.7 Job Listeners Execution
We can also attach listeners to the scheduler which have methods that are called before and after the execution of jobs. A listener implements the JobListener
interface and all its methods should contain a try-catch block and perform a small amount of work, otherwise, this might prevent the execution of the job.
Below we can see a Quartz application that uses listeners.
MyJobListener.java
package com.javacodegeeks.quartz.joblistener; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyJobListener implements JobListener { private final Logger log = LoggerFactory.getLogger(MyJobListener.class); public String getName() { return MyJobListener.class.getSimpleName(); } public void jobToBeExecuted(JobExecutionContext context) { try { String jobName = context.getJobDetail().getKey().toString(); log.info("{} is about to be executed", jobName ); } catch (Exception e) { log.error("Exception before job execution in listener", e); } } public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { try { String jobName = context.getJobDetail().getKey().toString(); log.info("{} finised execution", jobName); } catch (Exception e) { log.error("Exception after job execution in listener", e); } } public void jobExecutionVetoed(JobExecutionContext context) { try { String jobName = context.getJobDetail().getKey().toString(); log.info("{} was about to be executed but a TriggerListener vetoed it's execution", jobName); } catch (Exception e) { log.error("Exception during job execution veto in listener", e); } } }
MyJob.java
package com.javacodegeeks.quartz.joblistener; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyJob implements Job { private final Logger log = LoggerFactory.getLogger(MyJob.class); public void execute(JobExecutionContext context) throws JobExecutionException { String jobName = context.getJobDetail().getKey().toString(); log.info("{} is being executed", jobName); } }
QuartzJobListenerExample.java
package com.javacodegeeks.quartz.joblistener; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.impl.StdSchedulerFactory; public class QuartzJobListenerExample { public void run() throws Exception { SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); JobDetail job = JobBuilder.newJob(MyJob.class) .withIdentity("myJob", "group") .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger", "group") .build(); // add the MyJobListener to the scheduler scheduler.getListenerManager() .addJobListener(new MyJobListener()); scheduler.scheduleJob(job, trigger); scheduler.start(); // wait long enough to see the job execution Thread.sleep(3 * 1000); scheduler.shutdown(true); } public static void main(String[] args) throws Exception { QuartzJobListenerExample example = new QuartzJobListenerExample(); example.run(); } }
If we run the code above, then the ordering of the methods that would be executed are:
MyJobListener.jobToBeExecuted()
-> MyJob.execute()
-> MyJobListener.jobWasExecuted()
Let’s confirm that from the output:
Output
12:16:23.376 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor 12:16:23.407 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 12:16:23.407 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.2.1 created. 12:16:23.408 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized. 12:16:23.410 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.1) 'QuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 12:16:23.410 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'QuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties' 12:16:23.410 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.2.1 12:16:23.420 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED started. 12:16:23.421 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 12:16:23.427 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group.myJob', class=com.javacodegeeks.quartz.joblistener.MyJob 12:16:23.430 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers 12:16:23.430 [QuartzScheduler_Worker-1] INFO com.javacodegeeks.quartz.joblistener.MyJobListener - group.myJob is about to be executed 12:16:23.432 [QuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group.myJob 12:16:23.433 [QuartzScheduler_Worker-1] INFO com.javacodegeeks.quartz.joblistener.MyJob - group.myJob is being executed 12:16:23.433 [QuartzScheduler_Worker-1] INFO com.javacodegeeks.quartz.joblistener.MyJobListener - group.myJob finised execution 12:16:28.425 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutting down. 12:16:28.426 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED paused. 12:16:28.426 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutting down threadpool... 12:16:28.444 [QuartzScheduler_Worker-1] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 12:16:28.915 [QuartzScheduler_Worker-3] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 12:16:28.915 [QuartzScheduler_Worker-2] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 12:16:28.915 [main] DEBUG org.quartz.simpl.SimpleThreadPool - No executing jobs remaining, all threads stopped. 12:16:28.915 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutdown of threadpool complete. 12:16:28.916 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutdown complete.
3.8 Securing the Scheduler
There might be use cases where the scheduler APIs have to be exposed via web services, as jobs might be dynamically added or changed through a web interface. If those web services are accidentally exposed to the outside world, then this will cause a major security vulnerability as malicious users could take control of the scheduler and destroy your system by running native OS jobs.
As such, the scheduler APIs must never be exposed via public web services. If you have operators that create, update or delete jobs via a GUI, then the users should grant specific permissions to the GUI which should talk to the web services via a private network.
3.9 Skip Update Check
Quartz has a built-in functionality which checks for updates on startup, asynchronously in the background. This does not affect the initialization time but it is advised to be skipped for production systems.
There are 3 ways to skip the update check depending on the use case.
3.9.1 Programmatically
The property to skip the update check programmatically is the org.quartz.scheduler.skipUpdateCheck
and should be passed to the SchedulerFactory constructor. We should also set the property org.quartz.threadPool.threadCount
, otherwise the scheduler threads will be set to zero and Quartz will fail to start.
In the QuartzSkipUpdateCheckExample
class below we can see how to programmatically skip the update check.
QuartzSkipUpdateCheckExample.java
package com.javacodegeeks.quartz.skipupdatecheck; import java.util.Properties; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.impl.StdSchedulerFactory; import com.javacodegeeks.quartz.builders.SimpleJob; public class QuartzSkipUpdateCheckExample { public void run() throws Exception { Properties props = new Properties(); props.setProperty("org.quartz.scheduler.skipUpdateCheck", "true"); props.setProperty("org.quartz.threadPool.threadCount", "3"); SchedulerFactory schedulerFactory = new StdSchedulerFactory(props); Scheduler scheduler = schedulerFactory.getScheduler(); JobDetail job = JobBuilder.newJob(SimpleJob.class) .withIdentity("myJob", "myGroup") .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("myTrigger", "myGroup") .build(); scheduler.scheduleJob(job, trigger); scheduler.start(); // wait long enough to see the job execution Thread.sleep(5 * 1000); scheduler.shutdown(true); } public static void main(String[] args) throws Exception { QuartzSkipUpdateCheckExample example = new QuartzSkipUpdateCheckExample(); example.run(); } }
The job we use in the above code is the SimpleJob
we used in previous example. Let’s see though what would happen if we didn’t skip the update check on startup by setting the org.quartz.scheduler.skipUpdateCheck
to false and running the application.
Output
12:36:43.546 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor 12:36:43.570 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 12:36:43.574 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.2.1 created. 12:36:43.575 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized. 12:36:43.576 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.1) 'QuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 12:36:43.576 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'QuartzScheduler' initialized from an externally provided properties instance. 12:36:43.576 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.2.1 12:36:43.582 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED started. 12:36:43.583 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 12:36:43.585 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.javacodegeeks.quartz.builders.SimpleJob 12:36:43.589 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers 12:36:43.589 [QuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob 12:36:43.589 [QuartzScheduler_Worker-1] INFO com.javacodegeeks.quartz.builders.SimpleJob - SimpleJob executed! 12:36:44.573 [Timer-0] DEBUG org.quartz.utils.UpdateChecker - Checking for available updated version of Quartz... 12:36:44.738 [Timer-0] DEBUG org.quartz.utils.UpdateChecker - Quartz version update check failed: Server returned HTTP response code: 403 for URL: http://www.terracotta.org/kit/reflector?kitID=quartz&pageID=update.properties&id=2130706433&os-name=Mac+OS+X&jvm-name=Java+HotSpot%28TM%29+64-Bit+Server+VM&jvm-version=1.8.0_65&platform=x86_64&tc-version=2.2.1&tc-product=Quartz&source=Quartz&uptime-secs=1&patch=UNKNOWN 12:36:48.586 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutting down. 12:36:48.586 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED paused. 12:36:48.587 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutting down threadpool... 12:36:48.613 [QuartzScheduler_Worker-1] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 12:36:49.085 [QuartzScheduler_Worker-2] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 12:36:49.085 [QuartzScheduler_Worker-3] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 12:36:49.085 [main] DEBUG org.quartz.simpl.SimpleThreadPool - No executing jobs remaining, all threads stopped. 12:36:49.085 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutdown of threadpool complete. 12:36:49.085 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutdown complete.
In the above output, Quartz checks for available updated version (lines 21-22) which at the time this example ran, it failed with 403 HTTP error which indicates that the access to the requested URL was forbidden for some reason.
3.9.2 Configuration file
The configuration of Quartz is typically done through the quartz.properties file. The properties that we added programmatically to the previous example can be alternatively added to this file. In the project, this file is located under src/main/resources.
quartz.properties
org.quartz.scheduler.skipUpdateCheck=true org.quartz.threadPool.threadCount=3
3.9.3 System property
The third way to skip the update check is through the system environment
-Dorg.terracotta.quartz.skipUpdateCheck=true when starting the application. To add this to Eclipse, right click on QuartzSkipUpdateCheckExample.java
, select Run As -> Run Configurations, add the environment to the VM Arguments section and finally click Run to start the application:
3.10 Avoid scheduling Jobs at DST
A trigger can be scheduled to run at given moments in time, defined with Unix cron-like schedule definitions. Because cron triggers fire at given hours/minutes/seconds, they are subject to some oddities when Daylight Savings time (DST) transitions occur. Depending on your locale you must check when DST occurs and avoid setting a cron expression at that time, as this might result in duplicate job firing or even none. Note that the DST transitions only affect the cron triggers and not the other types of triggers.
It’s not very easy to reproduce a DST transition with an example, but we can take a look at how a cron trigger is created below.
QuartzSkipUpdateCheckExample.java
package com.javacodegeeks.quartz.crontrigger; import org.quartz.CronScheduleBuilder; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.impl.StdSchedulerFactory; import com.javacodegeeks.quartz.builders.SimpleJob; public class QuartzCronTriggerExample { public void run() throws Exception { SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); JobDetail job = JobBuilder.newJob(SimpleJob.class) .withIdentity("myJob", "myGroup") .build(); // Create the trigger and define a cron schedule of every 2 seconds. Avoid setting this to DST of your locale Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("myTrigger", "myGroup") .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?")) .build(); scheduler.scheduleJob(job, trigger); scheduler.start(); // wait long enough to see the job execution Thread.sleep(5 * 1000); scheduler.shutdown(true); } public static void main(String[] args) throws Exception { QuartzCronTriggerExample example = new QuartzCronTriggerExample(); example.run(); } }
In the above example we define a cron trigger using the CronScheduleBuilder
class which executes the SimpleJob
job every 2 seconds. This is exactly what the output shows:
Output
14:52:49.770 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor 14:52:49.792 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 14:52:49.793 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.2.1 created. 14:52:49.794 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized. 14:52:49.794 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.1) 'QuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 14:52:49.794 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'QuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties' 14:52:49.794 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.2.1 14:52:49.809 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED started. 14:52:49.812 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 14:52:50.015 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.javacodegeeks.quartz.builders.SimpleJob 14:52:50.018 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 14:52:50.018 [QuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob 14:52:50.019 [QuartzScheduler_Worker-1] INFO com.javacodegeeks.quartz.builders.SimpleJob - SimpleJob executed! 14:52:52.003 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.javacodegeeks.quartz.builders.SimpleJob 14:52:52.003 [QuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob 14:52:52.003 [QuartzScheduler_Worker-2] INFO com.javacodegeeks.quartz.builders.SimpleJob - SimpleJob executed! 14:52:52.004 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 14:52:54.005 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.javacodegeeks.quartz.builders.SimpleJob 14:52:54.006 [QuartzScheduler_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob 14:52:54.006 [QuartzScheduler_Worker-3] INFO com.javacodegeeks.quartz.builders.SimpleJob - SimpleJob executed! 14:52:54.006 [QuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 14:52:54.811 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutting down. 14:52:54.812 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED paused. 14:52:54.812 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutting down threadpool... 14:52:55.014 [QuartzScheduler_Worker-3] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 14:52:55.014 [QuartzScheduler_Worker-2] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 14:52:55.047 [QuartzScheduler_Worker-1] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down. 14:52:55.047 [main] DEBUG org.quartz.simpl.SimpleThreadPool - No executing jobs remaining, all threads stopped. 14:52:55.047 [main] DEBUG org.quartz.simpl.SimpleThreadPool - Shutdown of threadpool complete. 14:52:55.047 [main] INFO org.quartz.core.QuartzScheduler - Scheduler QuartzScheduler_$_NON_CLUSTERED shutdown complete.
3.11 Enable detailed Logging
In order to properly diagnose and trace issues in applications that use Quartz, we should take logging very seriously. Any code that gets executed inside jobs, job listeners etc must be logged. As we saw from all the previous examples, we tried to add logs to any method that was invoked, using the SLF4J and Logback libraries. In addition, Quartz has its own logs when an event occurs i.e. a scheduler gets created, a job gets executed etc. Those logs come from the org.quartz
package and should not be ignored by the logging properties we use.
4. Conclusion
In this post, we covered several best practices for Quartz by providing code examples. We mainly focused on the basic components of Quartz which are jobs, job details, triggers and listeners. We also took a look at a security concern, how to skip the update check which is advised for production systems and the need for detailed logging.
5. Download the Eclipse project
You can download the full source code of the above examples here: Java Quartz Best Practices Example
Do you have some expertise in running Quartz in cluster mode. Is the migration from NON_CLUSTERED mode to a clustered mode requires just updating the quartz.properties file and then restarting the instance and then subsequently add more instances?
Is there some other step required.