Skip to content

Instantly share code, notes, and snippets.

@franzwong
Last active February 5, 2023 08:12
Show Gist options
  • Save franzwong/0f60609d7b6c7e6f4c45e3af58c50fc1 to your computer and use it in GitHub Desktop.
Save franzwong/0f60609d7b6c7e6f4c45e3af58c50fc1 to your computer and use it in GitHub Desktop.
Retry with time interval for Quartz

Quartz only allows immediate retry. If we want to retry after a certain time interval (e.g. expontential backoff), we need to reschedule the current job. We also need to reset the schedule if it is back to normal.

package org.example;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public class DemoJob implements Job {
private static final Logger LOGGER = LoggerFactory.getLogger(DemoJob.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
try {
doSomething();
} catch (Exception e) {
handleError(e);
throw new JobExecutionException(e);
}
}
private void doSomething() throws Exception {
// Randomly fail
if (Math.random() * 3 < 1) {
throw new Exception("Error");
} else {
LOGGER.info("Success");
}
}
private void handleError(Exception e) {
LOGGER.error(e.getMessage());
}
}
package org.example;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;
import static org.quartz.CronScheduleBuilder.cronSchedule;
public class Main {
public static void main(String[] args) throws Exception {
var app = new Main();
app.execute();
}
private void execute() throws Exception {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
JobDetail job = JobBuilder.newJob(DemoJob.class)
.withIdentity("job1", "jobGroup1")
.build();
var trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "triggerGroup1")
.withSchedule(cronSchedule("0/20 * * * * ?"))
.build();
scheduler.scheduleJob(job, trigger);
scheduler.getListenerManager().addJobListener(new RetryJobListener());
scheduler.start();
}
}
package org.example;
import org.quartz.CronTrigger;
import org.quartz.DateBuilder;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.listeners.JobListenerSupport;
import java.time.Duration;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.DateBuilder.futureDate;
public class RetryJobListener extends JobListenerSupport {
public static final String CRON_EXPRESSION_KEY = "__RETRYJOBLISTENER_CRON_EXPRESSION__";
public static final String RETRIES_KEY = "__RETRYJOBLISTENER_RETRIES__";
@Override
public String getName() {
return "__RETRYJOBLISTENER__";
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
if (jobException == null) {
if (isRetry(context)) {
restoreSchedule(context);
}
} else {
rescheduleForRetry(context);
}
}
private boolean isRetry(JobExecutionContext context) {
var map = context.getJobDetail().getJobDataMap();
if (map.containsKey(RETRIES_KEY)) {
var retries = map.getInt(RETRIES_KEY);
return (retries > 0);
} else {
return false;
}
}
private void restoreSchedule(JobExecutionContext context) {
context.getJobDetail().getJobDataMap().put(RETRIES_KEY, 0);
var trigger = context.getTrigger();
var triggerKey = trigger.getKey();
var cronExpression = (String) trigger.getJobDataMap().get(CRON_EXPRESSION_KEY);
var newTrigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.withSchedule(cronSchedule(cronExpression))
.usingJobData(trigger.getJobDataMap())
.build();
try {
context.getScheduler().rescheduleJob(triggerKey, newTrigger);
} catch (SchedulerException schedulerException) {
throw new RuntimeException(schedulerException);
}
}
private void rescheduleForRetry(JobExecutionContext context) {
var jobDataMap = context.getJobDetail().getJobDataMap();
if (!jobDataMap.containsKey(RETRIES_KEY)) {
jobDataMap.put(RETRIES_KEY, 0);
}
var retries = jobDataMap.getInt(RETRIES_KEY);
var retryInterval = getRetryInterval(retries);
jobDataMap.put(RETRIES_KEY, retries + 1);
getLog().info(String.format("Error occurs, will retry after %d milliseconds", retryInterval.toMillis()));
var trigger = context.getTrigger();
var triggerKey = trigger.getKey();
var newTrigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.startAt(futureDate((int) retryInterval.toMillis(), DateBuilder.IntervalUnit.MILLISECOND))
.usingJobData(trigger.getJobDataMap())
.build();
if (trigger instanceof CronTrigger cronTrigger) {
// Backup cron expression for restoration
var cronExpression = cronTrigger.getCronExpression();
newTrigger.getJobDataMap().put(CRON_EXPRESSION_KEY, cronExpression);
}
try {
context.getScheduler().rescheduleJob(triggerKey, newTrigger);
} catch (SchedulerException schedulerException) {
throw new RuntimeException(schedulerException);
}
}
private Duration getRetryInterval(int retries) {
return Duration.ofMillis((long) (Math.pow(2, retries) * 1000 + Math.random() * 300));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment