Quartz

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

 

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 JobDataMapclass 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:

Java Quartz Best Practices - VM argument in Eclipse
VM argument in Eclipse to skip update check

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

Download
You can download the full source code of the above examples here: Java Quartz Best Practices Example

Lefteris Karageorgiou

Lefteris is a Lead Software Engineer at ZuluTrade and has been responsible for re-architecting the backend of the main website from a monolith to event-driven microservices using Java, Spring Boot/Cloud, RabbitMQ, Redis. He has extensive work experience for over 10 years in Software Development, working mainly in the FinTech and Sports Betting industries. Prior to joining ZuluTrade, Lefteris worked as a Senior Java Developer at Inspired Gaming Group in London, building enterprise sports betting applications for William Hills and Paddy Power. He enjoys working with large-scalable, real-time and high-volume systems deployed into AWS and wants to combine his passion for technology and traveling by attending software conferences all over the world.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Shaurya
Shaurya
4 years ago

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.

Back to top button