Quartz triggers, jobs, groups and more

If you are looking for a:

Trigger and Job names

During the configuration we may select for each job and each trigger a name and a group. The last values gives us a way to cluster. The first one is in interesting.

Job names

Setting manually a job names makes the code refactoring save. By default the spring quartz integration uses the bean name. Which is good for the start, but in case that we refactor the code and rename the bean we have to keep in mind to delete the old job in the quartz tables. As so it is usually saver to define a name, which we keep stable:

@Bean
    public JobDetailFactoryBean retryJob() {
        final var jobDetailFactory = new JobDetailFactoryBean();
        jobDetailFactory.setJobClass(RetryJob.class);
        // static name, we may of course also define a constant
        jobDetailFactory.setName("RETRY-JOB");
        jobDetailFactory.setDurability(true);
        return jobDetailFactory;
    }

If we now change the class name or packe structure it will get updated as soon we deploy the new version instead added to the job list.

Trigger names

The combination of a trigger name and trigger group has to be unique. Which gives us a very simple way to verify if a particular job was already scheduled or not. E.g. if we want to schedule a timer to notify a user that he has to change his password in 30 days we could just:

  public TriggerKey notifyUser(String user) throws SchedulerException {
        TriggerKey key = new TriggerKey(user, NotifyUserJob.ID);
        var t = TriggerBuilder.newTrigger()
                .forJob(JobKey.jobKey(NotifyUserJob.ID))
                .startAt(Date.from(Instant.now().plusMillis(1_000 * 60 * 60 * 24 * 30)))
                .usingJobData("user", user)
                .withIdentity(key)
                .build();
        scheduler.scheduleJob(t);
        return t.getKey();
    }

Now we get a ObjectAlreadyExistsException if we trigger it again and we can easily delete the trigger if the user changes the password before it would trigger. We may also update it.

Retry Triggers

A very common case is not only too trigger a task but also have a way to implement a kind of retry. With quartz this is pretty straight forward. In general we have to possibilities:

  1. Trigger immediately
  2. Queue a new Trigger to run the same job with a delay
Note: If we want to retry a job we need usually two transactions. One in which we rollback all changes, the business transaction and one more in which we update the job data and the intent to retry the job; the job transaction which we want to commit.

Trigger immediately

In the most simple way we can just catch any exception and re-trigger the job again. Any change to the JobData will be stored in an own transaction into the quartz tables. Which gives us a simple way to store intermediate results or just a retry counter; separately of our main application transaction and DB changes, which can rollback in peace.

public class RetryJob extends QuartzJobBean {

    @Autowired private YourService yourService;
    
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        Integer retryCount = (Integer)context.getMergedJobDataMap().getOrDefault("retryCount", 1);
        try {
            yourService.doStuff();
        } catch (Exception e) {
            if (retryCount >= 3) {
                throw e;
            } else {
                // don't use the getMergedJobDataMap of spring, it will not be updated
                context.getTrigger().getJobDataMap().put("retryCount", retryCount + 1);
                throw new JobExecutionException("yourService.doStuff failed  " 
                        + retryCount + " times. Will retry. " + e.getMessage(), 
                        true);
            }
        }
    }
}

Trigger later

Usually we want to wait a bit before we retry, which is just updating the trigger:

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        Integer retryCount = (Integer)context.getMergedJobDataMap().getOrDefault("retryCount", 1);
        try {
            yourService.doStuff();
        } catch (Exception e) {
            if (retryCount > 3) {
                throw e;
            } else {
                // we have to use the trgger data, as we repalce the trigger now
                context.getTrigger().getJobDataMap().put("retryCount", retryCount + 1);
                
                final var tb = context.getTrigger().getTriggerBuilder();
                tb.startAt(Date.from(Instant.now().plusSeconds(retryCount * 10)));
                scheduler.rescheduleJob(context.getTrigger().getKey(), tb.build());
            }
        }
    }

Example on Git.

Delete all job triggers

If we want to cancel all triggers for a job we can just list and delete them:

List<? extends Trigger> triggers = scheduler.getTriggersOfJob(xyzJob.getKey());
scheduler.unscheduleJobs(triggers.stream().map(t -> t.getKey()).toList());

Paul Sterl has written 54 articles

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>