springSeries · 1/42019년 10월 12일4 min read
Spring Quartz

Gracefully Shutting Down a Job Scheduler by Interrupting a Running Job in Quartz

How to gracefully shut down a Quartz scheduler by interrupting running jobs with a shutdown hook.

FFrank Advenoh
#quartz#spring#job

1. Introduction

This post is the fourth in the Quartz tutorial series and covers how to handle a Quartz server gracefully when shutting it down. When a shutdown event occurs, the internal interrupt() function is called on the running Quartz Job, and once notified by the interrupt, the developer can write the close logic as they see fit. You can kill the running thread (not recommended), or you can wait for the running Job and exclude it from the next schedule.

2. Development Environment

  • OS : Mac OS
  • IDE: Intellij
  • Java : JDK 1.8
  • Source code : github
  • Software management tool : Maven

3. Registering a ShutdownHook in Quartz and Implementing an Existing Job as an Interruptable Job

To gracefully shut down a running Job, you only need to configure two things.

3.1 Registering a ShutdownHook for the SchedulerFactoryBean in the Quartz Configuration

The SchedulerFactoryBean used in Quartz implements the SmartLifeCycle interface.

public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBean<Scheduler>,
BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle

Spring's SmartLifeCycle is a callback interface that has methods for various lifecycles, and the defined methods are called when the application shuts down or starts.

Register a Shutdown Hook by defining the gracefulShutdownHookForQuartz method as a bean in the QuartzConfiguration file.

@Bean
public SmartLifecycle gracefulShutdownHookForQuartz(@Qualifier("schedulerFactoryBean") SchedulerFactoryBean schedulerFactoryBean) {
   return new SmartLifecycle() {
      private boolean isRunning = false;
 
      @Override
      public boolean isAutoStartup() {
         return true;
      }
 
      @Override
      public void stop(Runnable callback) {
         stop();
         log.info("Spring container is shutting down.");
         callback.run();
      }
 
      @Override
      public void start() {
         log.info("Quartz Graceful Shutdown Hook started.");
         isRunning = true;
      }
 
      @Override
      public void stop() {
         isRunning = false;
 
         try {
            log.info("Quartz Graceful Shutdown...");
            interruptJobs(schedulerFactoryBean);
            schedulerFactoryBean.destroy();
         } catch (SchedulerException e) {
            try {
               log.info("Error shutting down Quartz: ", e);
               schedulerFactoryBean.getScheduler().shutdown(false);
            } catch (SchedulerException ex) {
               log.error("Unable to shutdown the Quartz scheduler.", ex);
            }
         }
      }
 
      @Override
      public boolean isRunning() {
         return isRunning;
      }
 
      @Override
      public int getPhase() {
         return Integer.MAX_VALUE;
      }
   };
}

Since we need to shut down gracefully, the method we are interested in is the stop() method. When this method is called, it queries all currently running Jobs in the Quartz scheduler and calls the interrupt() method of the running Jobs. I'll explain how the Job can handle this in the next section.

private void interruptJobs(SchedulerFactoryBean schedulerFactoryBean) throws SchedulerException {
   Scheduler scheduler = schedulerFactoryBean.getScheduler();
   for (JobExecutionContext jobExecutionContext : scheduler.getCurrentlyExecutingJobs()) {
      final JobDetail jobDetail = jobExecutionContext.getJobDetail();
      log.info("interrupting job :: jobKey : {}", jobDetail.getKey());
      scheduler.interrupt(jobDetail.getKey());
   }
}

3.2 Implementing a Quartz Job by Implementing the InterruptableJob Interface

To implement an interruptable Job, you implement the InterruptableJob interface and implement the interrupt() method. As you may have already guessed, on shutdown it is called by the stop() method of the SmartLifeCycle defined in 3.1, and it interrupts the thread of the currently running Job.

public class CronJob2 extends QuartzJobBean implements InterruptableJob {
    private volatile boolean isJobInterrupted = false;
(omitted) 
    @Override
    public void executeInternal(JobExecutionContext context) throws JobExecutionException {
        JobKey jobKey = context.getJobDetail().getKey();
        if (!isJobInterrupted) { //use the flag value to exclude it from the next schedule
(omitted)...
        }
    }
 
    @Override
    public void interrupt() {
        isJobInterrupted = true; //set the flag to indicate it has been interrupted
        if (currThread != null) {
            log.info("interrupting - {}", currThread.getName());
            currThread.interrupt(); //if the thread is paused, wake it up immediately to run
        }
    }
}

4. Wrap-up

We looked at how to gracefully shut down a running Job. The next post is the last in the Quartz tutorial series, where we'll look at implementing the Quartz admin UI.

5. References

관련 글