Setup Quartz on Heroku

There are two ways to use Quartz on Heroku:
1. Setup your own Quartz on Heroku
2. Use Heroku Quartz Addon

1. Setup your own Quartz on Heroku

Create Postgres db:
heroku addons:add heroku-postgresql:dev

Get Postgres properties:
heroku config

Use these properties to specify your:

  • org.quartz.dataSource.dataSource.URL
  • org.quartz.dataSource.dataSource.user
  • org.quartz.dataSource.dataSource.password

Your quartz.properties file

org.quartz.scheduler.instanceName = BatchScheduler
org.quartz.threadPool.threadCount = 3
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = dataSource
org.quartz.dataSource.dataSource.driver = org.postgresql.Driver
org.quartz.dataSource.dataSource.URL = jdbc:postgresql://ec2-23-21-133-106.compute-1.amazonaws.com:5432/dsqpq2m83g9jc
org.quartz.dataSource.dataSource.user = YOURUSERNAME
org.quartz.dataSource.dataSource.password = YOURPASSWORD
org.quartz.dataSource.dataSource.maxConnections = 1 

Create your Quartz tables in Postgres:
heroku pg:psql

Copy your Postgres query in the console:
Quartz tables for Quartz 2.1.6

Make sure you are using the same version of tables as specified in your pom.xml in this case my pom looks like:

<dependency>
      <groupId>org.quartz-scheduler</groupId>
      <artifactId>quartz</artifactId>
      <version>2.1.6</version>
</dependency>

2. Use Heroku Quartz Addon

Add Heroku scheduler to your project:
heroku addons:add scheduler:standard

Add the Quartz class Heroku must call in your Procfile:
scheduler: java $JAVA_OPTS -cp target/classes:target/dependency/* com.example.service.SchedulerService

Example of SchedulerService:

package com.example.service;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.repeatSecondlyForever;
import static org.quartz.TriggerBuilder.newTrigger;

public class SchedulerService {
	
    final static Logger logger = LoggerFactory.getLogger(SchedulerService.class);
    public static void main(String[] args) throws Exception {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.start();
        JobDetail jobDetail = newJob(DatabaseJob.class).build();
        Trigger trigger = newTrigger()
                .startNow()
                .withSchedule(repeatSecondlyForever(5))
                .build();
        scheduler.scheduleJob(jobDetail, trigger);
    }

    public static class DatabaseJob implements Job {
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            try {
              //Add your job that needs to execute here
            }
            catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }
}

Spring Quartz Service Implementation

Spring Service implementation of Quartz

package com.example.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.example.job.QuartzDeleteJob;
import com.example.job.QuartzInsertJob;

@Service
public class JobManagerService {

	public static final String BATCHINSERT = "QuartzInsertJob";
	public static final String BATCHDELETE = "QuartzDeleteJob";
	public static final String BATCHUPDATE = "QuartzUpdateJob";
	
	private Logger logger = LoggerFactory.getLogger(JobManagerService.class);
	
	private Scheduler scheduler = null;
	private Map<String, JobDetail> jobDetails = new HashMap<String, JobDetail>();
	private Map<String, Trigger> jobTriggers = new HashMap<String, Trigger>();
	private Map<String, JobKey> jobKeys = new HashMap<String, JobKey>();
	private Map<String, String> cronExpressions = new HashMap<String, String>();
	private List<String> cronOutput = new ArrayList<String>();
	
	private boolean successfullyInitialized = false;

	@PostConstruct
	public void initialize() {
		try {
			initializeScheduler();
			initializeJob(QuartzInsertJob.class, BATCHINSERT);
			initializeJob(QuartzDeleteJob.class, BATCHDELETE);
			successfullyInitialized = true;
		} catch (SchedulerException e) {
			logger.warn("exception during initialization");
			e.printStackTrace();
		}
	}
	
	private void initializeScheduler() throws SchedulerException {
		logger.info("initializing scheduler...");
		scheduler = StdSchedulerFactory.getDefaultScheduler();
		scheduler.start();
		logger.info("scheduler initialized and started");
	}

	public JobDetail buildJob(Class<?> jobClass, String jobName){
		JobDetail jobDetail =  JobBuilder.newJob((Class<Job>) jobClass).withIdentity(jobName).storeDurably(true).build();
		jobDetails.put(jobName, jobDetail);
		return jobDetail;
	}
	
	public void getJobKeyAndCheckExist(JobDetail jobDetail, String jobName) throws SchedulerException{
		JobKey jobKey = jobDetail.getKey();
		jobKeys.put(jobName, jobKey);
		
		if (scheduler.checkExists(jobKey)) {
			jobDetail = scheduler.getJobDetail(jobKey);
		
			List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobDetail.getKey());
			
			if (triggers.size() > 0) {
				if (triggers.size() > 1) {
					resetTriggers(jobName, triggers);
				} else {
					Trigger trigger = triggers.get(0);
					jobTriggers.put(jobName, trigger);
					
					cronExpressions.put(jobName, ((CronTrigger) trigger).getCronExpression());
				}
			}
		} else {
			scheduler.addJob(jobDetail, false);
		}
		
	}
	
	private void initializeJob(Class<?> jobClass, String jobName) throws SchedulerException {
		logger.info("initializing <{}>", jobName);
		JobDetail jobDetail = buildJob(jobClass, jobName);
		getJobKeyAndCheckExist(jobDetail, jobName);
		logger.info("<{}> initialized", jobName);
	}
	
	private void resetTriggers(String jobName, List<? extends Trigger> triggers) throws SchedulerException {			
		for (Trigger trigger : triggers) {
			scheduler.unscheduleJob(trigger.getKey());
		}
	}
	
	public String checkAndUnscheduleTrigger(String jobName) {
		Trigger trigger = jobTriggers.get(jobName);
		
		if (trigger != null) {
			TriggerKey triggerKey = trigger.getKey();
			
			logger.info("unscheduling <{}>", triggerKey);
			
			try {
				scheduler.unscheduleJob(trigger.getKey());
				jobTriggers.put(jobName, null);
				return triggerKey + " successfully unscheduled";
			} catch (SchedulerException e) {
				logger.error("exception unscheduling <{}>", triggerKey);
				e.printStackTrace();
			}
		} else {
			logger.info("trigger for <{}> not found", jobName);
		}
		return "exception unscheduling";
	}
	
	public List<String> checkAndScheduleCronExpression(String jobName, String triggerName, String cronExpression) {
		Trigger trigger = jobTriggers.get(jobName);
		
		if (trigger == null) {
			trigger = TriggerBuilder.newTrigger()
					.withIdentity(triggerName)
					.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
					.forJob(jobDetails.get(jobName)).build();
			
			try {
				cronOutput.add("scheduling "  + jobName + " with cron expression " + cronExpression);
				scheduler.scheduleJob(trigger);
				cronOutput.add("successfully scheduled: " + jobName);
				
				jobTriggers.put(jobName, trigger);
				cronExpressions.put(jobName, cronExpression);
			} catch (SchedulerException e) {
				cronOutput.add("exception scheduling: "+ jobName);
				e.printStackTrace();
			}
		} else {
			cronOutput.add("trigger already exists for: " + jobName);
		}
		
		return cronOutput;
	}
	
	public boolean isSuccessfullyInitialized() {
		return successfullyInitialized;
	}

	public String retrieveCronExpression(String jobName) {
		return cronExpressions.get(jobName);
	}
	
	public Map<String, String> getCronExpressions(){
		return cronExpressions;
	}
	
	public Map<String, Trigger> getJobTriggers(){
		return jobTriggers;
	}
	
	public Map<String, JobDetail> getJobDetails(){
		return jobDetails;
	}
	
	public Map<String, JobKey> getJobKeys(){
		return jobKeys;
	}
	
	public List<String> getCurrentlyExecutingJobs() throws SchedulerException{
		List<String> jobDescriptions = new ArrayList<String>();
		for (JobExecutionContext runningJobs : scheduler.getCurrentlyExecutingJobs()){
			jobDescriptions.add(runningJobs.getJobDetail().getDescription());
		}
		return jobDescriptions;
	}
}

Example of an Insert Quartz job to insert records into a database using jpa.

package com.example.job;

import java.util.List;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.example.jpa.JpaConfiguration;
import com.example.model.Customer;
import com.example.service.CustomerService;

public class QuartzDeleteJob implements Job  {

	@Override
	public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
		  
	        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
	        applicationContext.getEnvironment().setActiveProfiles("default");
	        applicationContext.scan(JpaConfiguration.class.getPackage().getName());
	        applicationContext.refresh();
	        CustomerService customerService = applicationContext.getBean(CustomerService.class);
	        
	        List<Customer> listOfCustomersToDelete = customerService.getPaginatedCustomer(0, 2);
	        customerService.deleteCustomers(listOfCustomersToDelete);
	}
}