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.