Configuring a Quartz Scheduler in a clustered Spring Boot application

February 11, 2019 spring, quartz

A few weeks ago, I was helping a colleague who had to fix a failing ‘Quartz job.' The ‘Spring Boot' application was running in a cluster and configured to use a ‘JDBC JobStore', but regardless of this config, the job was triggered on each node. After some digging in the code, we discovered that the configuration contained a lot of copy-paste spaghetti code from StackOverflow. While investigating the problem, we discovered that the Spring Boot documentation for Quartz is brief and lacks basic examples. StackOverflow gave us a lot of input but, sadly, without any good solutions.

After this investigation, I decided to dig into the code for Spring Boot and write a small proof of concept for how to configure Quartz in a cluster.

First, add the required dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
</dependency>

Second, configure Quartz within the application properties:

spring.quartz.job-store-type=jdbc
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO

The ‘instanceId' property will automatically generate a random name for your node.

When you start the application, you can verify that the setup is correct by searching in the logs for the following statement:

2019-02-01 13:39:17.600  INFO 11310 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler meta-data: Quartz Scheduler (v2.3.0) 'quartzScheduler' with instanceId 'Jeroens-MacBook-Pro.local1549024757590'
  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 10 threads.
  Using job-store 'org.springframework.scheduling.quartz.LocalDataSourceJobStore' - which supports persistence. and is clustered.

After this, you can create a 'Job'; note that Spring dependency injection is working at this point!

import com.example.quartz.service.AService;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.beans.factory.annotation.Autowired;

public class AJob implements Job {

    @Autowired
    private AService aService;

    @Override
    public void execute(JobExecutionContext context) {
        aService.printTime();
    }
}

Last, configure a trigger and a ‘JobDetail' bean:

import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfigureJob {

    @Bean
    public JobDetail jobADetails() {
        return JobBuilder.newJob(AJob.class).withIdentity("sampleJobA")
                .storeDurably().build();
    }

    @Bean
    public Trigger jobATrigger(JobDetail jobADetails) {

        return TriggerBuilder.newTrigger().forJob(jobADetails)

                .withIdentity("sampleTriggerA")
                .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * ? * * *"))
                .build();
    }
}

Start the application, and the job will run.
As it turns out, Spring Boot does a lot of the configuring for us. It will create a scheduler and all other Quartz configurations. No need to do it yourself!

After we used this POC to configure the failing application, we discovered that another microservice was reusing the same Quartz tables. Reusing the same data source for multiple services is—as you all know—a bad practice. However, if you need to make this work, configure each service to use its own set of Quartz tables. This can be done with a table prefix.

Have fun using Quartz and Spring Boot!

All code is available on github.

Jeroen Bellen
Jeroen Bellen
Alken