Scheduler

The scheduler runs jobs at a specified time. On multi-node application server clusters, it is high-available: jobs will be executed on any one node of the cluster, regardless of where the job was created. Jobs are persisted to the database and survive application server restarts.

skp-server

The technology stack in use is Quartz Scheduler. An instance of the scheduler is provided by dependency injection. The scheduler only runs at runlevel 3; at lower runlevels it is either paused or completely shutdown. In other words: while an application server is in maintenance (runlevel 2), it will not execute any scheduled jobs.

Java source code

To obtain an instance of the scheduler, it can be injected:

@Inject
Scheduler scheduler;

The action that is run by the scheduler is called a job. It is suggested to create jobs in the com.skalio.skp.server.schedulder package. Jobs extend org.quartz.Job and can be dependency-injection-managed services or simple POJOs.

@Service
@PerLookup
public class HelloWorldJob implements Job {
    private final Logger log = LoggerFactory.getLogger(getClass());

    @PostConstruct
    public void setup() {
        // called before every job execution
        log.info("I'm born!");
    }

    @PreDestroy
    public void shutdown() {
        // called after every job execution
        log.info("And I'm dead again");
    }

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("Hello, world!");
    }

}

To make a job-class useable, it must be registered with the scheduler under a unique job-key to create a job-instance (aka JobDetail). A job-key consists of two arbitrary strings: job-name and job-group.

A trigger defines when and how often a job-instance is to be executed. It must be registered with the scheduler under a unique trigger-key. A trigger-key consists of two arbitrary strings: trigger-name and trigger-group.

JobDetail job = JobBuilder.newJob(HelloWorldJob.class)
    .withIdentity("helloWorldJob", "examples")
    .build();
Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("helloWorldTrigger", "examples")
    .startNow()
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
            .withIntervalInSeconds(10)
            .withRepeatCount(3)
    )
    .build();
scheduler.scheduleJob(job, trigger);

A job-instance without a trigger is removed from the scheduler automatically, unless it is created durably. This allows pre-registration of jobs during application server startup.

public static void preRegister(Scheduler scheduler) throws SchedulerException {
    JobDetail jobDetail = JobBuilder.newJob(SimpleNotificationJob.class)
            .withIdentity(JOB_KEY)
            .requestRecovery()
            .storeDurably()
            .build();

    scheduler.addJob(jobDetail, true);
}

Pre-registration of jobs should be done in SchedulerShutdownManager#setup(), which is executed when entering runlevel 1. When registring a job, make sure that the job either replaces a potentially existing jobKey, or explicitely deletes existing jobs and/or triggers.

At a later time, one or more triggers can be setup to reference the job.

String json = notificationService.create(Notification.Type.folder_activity)
        .withRecipient(userDAO.getEmail())
        .withFolderIdxList(Arrays.asList(rootFolderDAO.getIdx()))
        .getNotificationInJson();

Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity(triggerKey)
        .forJob(SimpleNotificationJob.JOB_KEY)
        .startAt(Date.from(timestamp))
        .usingJobData(SimpleNotificationJob.JOBDATA_KEY_NOTIFICATION_JSON, json)
        .build();

Date firesAt = scheduler.scheduleJob(trigger);
log.debug("Trigger {} for {} scheduled, will fire at {}", triggerKey, trigger.getJobKey(), firesAt);

In this example, the SimpleNotificationJob once triggered will receive job-instance-specific data (here: a json-encoded Notification). This jobData is persisted in the schedulers database backend. As a result, only primitive or serializable objects shall be attached to the jobData. It is also advisable to use a tolerant serialization/deserialization concept (such a JSON) to allow a potential future version of the job to still process the data that was serialized for it some time in the past.

The trigger-provided jobData is made available to the job via the JobExecutionContext#getMergedJobDataMap(). Make sure to test for NPEs.

public class SimpleNotificationJob implements Job {

  [...]

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobKey key = context.getJobDetail().getKey();
        JobDataMap dataMap = context.getMergedJobDataMap();
        notificationInJson = dataMap.getString(JOBDATA_KEY_NOTIFICATION_JSON);

        if (notificationInJson == null) {
            log.warn("No notification payload found!");
            throw new JobExecutionException("No notification payload found", false);
        }

  [...]

View list of scheduled jobs

The maintenance port offers a human-readable list of currently scheduled jobs at http://application-server:8082/scheduler. Since the scheduler is high-available and cluster aware, it shows all jobs, regardless of the application server they were created on or are running on.