package com.thinkbiganalytics.scheduler;
/*-
* #%L
* thinkbig-scheduler-quartz
* %%
* Copyright (C) 2017 ThinkBig Analytics
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import com.thinkbiganalytics.cluster.ClusterService;
import com.thinkbiganalytics.scheduler.model.DefaultJobIdentifier;
import com.thinkbiganalytics.scheduler.model.DefaultJobInfo;
import com.thinkbiganalytics.scheduler.model.DefaultTriggerIdentifier;
import com.thinkbiganalytics.scheduler.model.DefaultTriggerInfo;
import com.thinkbiganalytics.scheduler.util.CronExpressionUtil;
import org.apache.commons.lang3.BooleanUtils;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerMetaData;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
/**
* Quartz implementation of the JobScheduler
*/
@Service
public class QuartzScheduler implements JobScheduler {
@Autowired
@Qualifier("schedulerFactoryBean")
SchedulerFactoryBean schedulerFactoryBean;
@Inject
private QuartzClusterMessageSender clusterMessageSender;
private Set<JobSchedulerListener> listeners = new HashSet<>();
public static JobIdentifier jobIdentifierForJobKey(JobKey jobKey) {
return new DefaultJobIdentifier(jobKey.getName(), jobKey.getGroup());
}
public static JobKey jobKeyForJobIdentifier(JobIdentifier jobIdentifier) {
return new JobKey(jobIdentifier.getName(), jobIdentifier.getGroup());
}
public static TriggerKey triggerKeyForTriggerIdentifier(TriggerIdentifier triggerIdentifier) {
return new TriggerKey(triggerIdentifier.getName(), triggerIdentifier.getGroup());
}
public static TriggerIdentifier triggerIdentifierForTriggerKey(TriggerKey triggerKey) {
return new DefaultTriggerIdentifier(triggerKey.getName(), triggerKey.getGroup());
}
public Scheduler getScheduler() {
return schedulerFactoryBean.getScheduler();
}
private JobDetail getJobDetail(JobIdentifier jobIdentifier, Object task, String runMethod)
throws NoSuchMethodException, ClassNotFoundException {
MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
bean.setTargetObject(task);
bean.setTargetMethod(runMethod);
bean.setName(jobIdentifier.getName());
bean.setGroup(jobIdentifier.getGroup());
bean.afterPropertiesSet();
return bean.getObject();
}
@Override
public void scheduleWithCronExpressionInTimeZone(JobIdentifier jobIdentifier, Runnable task, String cronExpression,
TimeZone timeZone) throws JobSchedulerException {
scheduleWithCronExpressionInTimeZone(jobIdentifier, task, "run", cronExpression, null);
}
@Override
public void scheduleWithCronExpression(JobIdentifier jobIdentifier, Runnable task, String cronExpression)
throws JobSchedulerException {
scheduleWithCronExpressionInTimeZone(jobIdentifier, task, "run", cronExpression, null);
}
public void scheduleWithCronExpressionInTimeZone(JobIdentifier jobIdentifier, Object task, String runMethod,
String cronExpression, TimeZone timeZone) throws JobSchedulerException {
try {
JobDetail jobDetail = getJobDetail(jobIdentifier, task, runMethod);
if (timeZone == null) {
timeZone = TimeZone.getDefault();
}
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(new TriggerKey("trigger_" + jobIdentifier.getUniqueName(), jobIdentifier.getGroup()))
.forJob(jobDetail)
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)
.inTimeZone(timeZone)
.withMisfireHandlingInstructionFireAndProceed()).build();
scheduleJob(jobDetail, trigger);
} catch (Exception e) {
throw new JobSchedulerException();
}
}
@Override
public void schedule(JobIdentifier jobIdentifier, Runnable task, Date startTime) throws JobSchedulerException {
schedule(jobIdentifier, task, "run", startTime);
}
public void schedule(JobIdentifier jobIdentifier, Object task, String runMethod, Date startTime) throws JobSchedulerException {
scheduleWithFixedDelay(jobIdentifier, task, runMethod, startTime, 0L);
}
@Override
public void scheduleWithFixedDelay(JobIdentifier jobIdentifier, Runnable runnable, long startDelay)
throws JobSchedulerException {
scheduleWithFixedDelay(jobIdentifier, runnable, "run", new Date(), 0L);
}
@Override
public void scheduleWithFixedDelay(JobIdentifier jobIdentifier, Runnable runnable, Date startTime, long startDelay)
throws JobSchedulerException {
scheduleWithFixedDelay(jobIdentifier, runnable, "run", startTime, startDelay);
}
public void scheduleWithFixedDelay(JobIdentifier jobIdentifier, Object task, String runMethod, Date startTime, long startDelay)
throws JobSchedulerException {
try {
JobDetail jobDetail = getJobDetail(jobIdentifier, task, runMethod);
Date triggerStartTime = startTime;
if (startDelay > 0L || startTime == null) {
triggerStartTime = new Date(System.currentTimeMillis() + startDelay);
}
Trigger
trigger =
newTrigger().withIdentity(new TriggerKey(jobIdentifier.getName(), jobIdentifier.getGroup())).forJob(jobDetail)
.startAt(triggerStartTime).build();
getScheduler().scheduleJob(jobDetail, trigger);
triggerListeners(JobSchedulerEvent.scheduledJobEvent(jobIdentifier));
} catch (Exception e) {
throw new JobSchedulerException();
}
}
@Override
public void scheduleAtFixedRate(JobIdentifier jobIdentifier, Runnable runnable, Date startTime, long period)
throws JobSchedulerException {
scheduleAtFixedRate(jobIdentifier, runnable, "run", startTime, period);
}
@Override
public void scheduleAtFixedRate(JobIdentifier jobIdentifier, Runnable runnable, long period) throws JobSchedulerException {
scheduleAtFixedRate(jobIdentifier, runnable, "run", new Date(), period);
}
public void scheduleAtFixedRate(JobIdentifier jobIdentifier, Object task, String runMethod, Date startTime, long period)
throws JobSchedulerException {
scheduleAtFixedRateWithDelay(jobIdentifier, task, runMethod, startTime, period, 0L);
}
public void scheduleAtFixedRateWithDelay(JobIdentifier jobIdentifier, Object task, String runMethod, Date startTime,
long period, long startDelay) throws JobSchedulerException {
JobDetail jobDetail = null;
try {
jobDetail = getJobDetail(jobIdentifier, task, runMethod);
Date triggerStartTime = startTime;
if (startDelay > 0L || startTime == null) {
triggerStartTime = new Date(System.currentTimeMillis() + startDelay);
}
Trigger
trigger =
TriggerBuilder.newTrigger().withIdentity(new TriggerKey(jobIdentifier.getName(), jobIdentifier.getGroup()))
.forJob(jobDetail).startAt(triggerStartTime)
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMilliseconds(period).repeatForever()).build();
getScheduler().scheduleJob(jobDetail, trigger);
triggerListeners(JobSchedulerEvent.scheduledJobEvent(jobIdentifier));
} catch (NoSuchMethodException | ClassNotFoundException | SchedulerException e) {
throw new JobSchedulerException("Error calling scheduleAtFixedRateWithDelay", e);
}
}
@Override
public Date getNextFireTime(String cronExpression) throws ParseException {
return CronExpressionUtil.getNextFireTime(cronExpression);
}
@Override
public Date getNextFireTime(Date lastFireTime, String cronExpression) throws ParseException {
return CronExpressionUtil.getNextFireTime(lastFireTime, cronExpression);
}
@Override
public List<Date> getNextFireTimes(String cronExpression, Integer count) throws ParseException {
return CronExpressionUtil.getNextFireTimes(cronExpression, count);
}
@Override
public Date getPreviousFireTime(String cronExpression) throws ParseException {
return CronExpressionUtil.getPreviousFireTime(cronExpression);
}
@Override
public Date getPreviousFireTime(Date lastFireTime, String cronExpression) throws ParseException {
return CronExpressionUtil.getPreviousFireTime(lastFireTime, cronExpression);
}
@Override
public List<Date> getPreviousFireTimes(String cronExpression, Integer count) throws ParseException {
return CronExpressionUtil.getPreviousFireTimes(cronExpression, count);
}
/**
* Start the Scheduler after it has been Paused Misfires on Triggers during the pause time will be ignored
*/
@Override
public void startScheduler() throws JobSchedulerException {
try {
getScheduler().start();
clusterMessageSender.notifySchedulerResumed();
triggerListeners(JobSchedulerEvent.schedulerStartedEvent());
} catch (SchedulerException e) {
throw new JobSchedulerException("Unable to Start the Scheduler", e);
}
}
/**
* Pause the Scheduler and Halts the firing of all Triggers Misfires on Triggers during the pause time will be ignored
*/
@Override
public void pauseScheduler() throws JobSchedulerException {
try {
getScheduler().standby();
clusterMessageSender.notifySchedulerPaused();
} catch (SchedulerException e) {
throw new JobSchedulerException("Unable to Pause the Scheduler", e);
}
}
@Override
public void triggerJob(JobIdentifier jobIdentifier) throws JobSchedulerException {
try {
getScheduler().triggerJob(jobKeyForJobIdentifier(jobIdentifier));
triggerListeners(JobSchedulerEvent.triggerJobEvent(jobIdentifier));
} catch (SchedulerException e) {
throw new JobSchedulerException("Unable to Trigger the Job " + jobIdentifier, e);
}
}
public void pauseTriggersOnJob(JobIdentifier jobIdentifier) throws JobSchedulerException {
try {
JobKey jobKey = jobKeyForJobIdentifier(jobIdentifier);
List<? extends Trigger> jobTriggers = getScheduler().getTriggersOfJob(jobKey);
if (jobTriggers != null) {
for (Trigger trigger : jobTriggers) {
TriggerIdentifier triggerIdentifier = triggerIdentifierForTriggerKey(trigger.getKey());
try {
pauseTrigger(triggerIdentifier);
} catch (JobSchedulerException e) {
}
}
triggerListeners(JobSchedulerEvent.pauseJobEvent(jobIdentifier));
clusterMessageSender.notifyJobPaused(jobIdentifier);
}
} catch (SchedulerException e) {
throw new JobSchedulerException("Unable to pause Active Triggers the Job " + jobIdentifier, e);
}
}
public void resumeTriggersOnJob(JobIdentifier jobIdentifier) throws JobSchedulerException {
try {
JobKey jobKey = jobKeyForJobIdentifier(jobIdentifier);
List<? extends Trigger> jobTriggers = getScheduler().getTriggersOfJob(jobKey);
if (jobTriggers != null) {
for (Trigger trigger : jobTriggers) {
TriggerIdentifier triggerIdentifier = triggerIdentifierForTriggerKey(trigger.getKey());
try {
resumeTrigger(triggerIdentifier);
} catch (JobSchedulerException e) {
}
}
triggerListeners(JobSchedulerEvent.resumeJobEvent(jobIdentifier));
clusterMessageSender.notifyJobResumed(jobIdentifier);
}
} catch (SchedulerException e) {
throw new JobSchedulerException("Unable to resume paused Triggers the Job " + jobIdentifier, e);
}
}
@Override
public void resumeTrigger(TriggerIdentifier triggerIdentifier) throws JobSchedulerException {
try {
getScheduler().resumeTrigger(triggerKeyForTriggerIdentifier(triggerIdentifier));
} catch (SchedulerException e) {
throw new JobSchedulerException("Unable to Resume the Trigger " + triggerIdentifier, e);
}
}
@Override
public void pauseTrigger(TriggerIdentifier triggerIdentifier) throws JobSchedulerException {
try {
getScheduler().pauseTrigger(triggerKeyForTriggerIdentifier(triggerIdentifier));
} catch (SchedulerException e) {
throw new JobSchedulerException("Unable to Pause the Trigger " + triggerIdentifier, e);
}
}
public void updateTrigger(TriggerIdentifier triggerIdentifier, String cronExpression) throws JobSchedulerException {
CronTrigger
trigger =
newTrigger().withIdentity(triggerIdentifier.getName(), triggerIdentifier.getGroup())
.withSchedule(cronSchedule(cronExpression)).build();
try {
updateTrigger(triggerIdentifier, trigger);
} catch (SchedulerException e) {
throw new JobSchedulerException(e);
}
}
@Override
public void deleteJob(JobIdentifier jobIdentifier) throws JobSchedulerException {
try {
getScheduler().deleteJob(jobKeyForJobIdentifier(jobIdentifier));
triggerListeners(JobSchedulerEvent.deleteJobEvent(jobIdentifier));
} catch (SchedulerException e) {
throw new JobSchedulerException("Unable to Delete the Job " + jobIdentifier, e);
}
}
public JobInfo buildJobInfo(JobDetail jobDetail) {
JobInfo detail = new DefaultJobInfo(jobIdentifierForJobKey(jobDetail.getKey()));
detail.setDescription(jobDetail.getDescription());
detail.setJobClass(jobDetail.getJobClass());
detail.setJobData(jobDetail.getJobDataMap().getWrappedMap());
return detail;
}
private TriggerInfo buildTriggerInfo(JobIdentifier jobIdentifier, Trigger trigger) {
TriggerInfo triggerInfo = new DefaultTriggerInfo(jobIdentifier, triggerIdentifierForTriggerKey(trigger.getKey()));
triggerInfo.setDescription(trigger.getDescription());
triggerInfo.setTriggerClass(trigger.getClass());
String cronExpression = null;
triggerInfo.setCronExpressionSummary("");
if (trigger instanceof CronTrigger) {
CronTrigger ct = (CronTrigger) trigger;
cronExpression = ct.getCronExpression();
triggerInfo.setCronExpressionSummary(ct.getExpressionSummary());
}
boolean
isSimpleTrigger =
(!CronTrigger.class.isAssignableFrom(trigger.getClass()) && SimpleTrigger.class.isAssignableFrom(trigger.getClass()));
triggerInfo.setSimpleTrigger(isSimpleTrigger);
boolean isScheduled = CronTrigger.class.isAssignableFrom(triggerInfo.getTriggerClass());
triggerInfo.setScheduled(isScheduled);
triggerInfo.setCronExpression(cronExpression);
triggerInfo.setNextFireTime(trigger.getNextFireTime());
triggerInfo.setStartTime(trigger.getStartTime());
triggerInfo.setEndTime(trigger.getEndTime());
//triggerInfo.setFinalFireTime(trigger.getFinalFireTime());
triggerInfo.setPreviousFireTime(trigger.getPreviousFireTime());
return triggerInfo;
}
@Override
public List<JobInfo> getJobs() throws JobSchedulerException {
List<JobInfo> list = new ArrayList<JobInfo>();
Scheduler sched = getScheduler();
try {
// enumerate each job group
for (String group : sched.getJobGroupNames()) {
// enumerate each job in group
for (JobKey jobKey : sched.getJobKeys(GroupMatcher.jobGroupEquals(group))) {
JobDetail jobDetail = sched.getJobDetail(jobKey);
JobInfo detail = buildJobInfo(jobDetail);
list.add(detail);
List<? extends Trigger> jobTriggers = sched.getTriggersOfJob(jobKey);
List<TriggerInfo> triggerInfoList = new ArrayList<TriggerInfo>();
if (jobTriggers != null) {
for (Trigger trigger : jobTriggers) {
TriggerInfo triggerInfo = buildTriggerInfo(detail.getJobIdentifier(), trigger);
Trigger.TriggerState state = sched.getTriggerState(trigger.getKey());
triggerInfo.setState(TriggerInfo.TriggerState.valueOf(state.name()));
triggerInfoList.add(triggerInfo);
}
}
detail.setTriggers(triggerInfoList);
}
}
} catch (SchedulerException e) {
}
return list;
}
/**
* Pause all jobs and triggers.
* {@link this#resumeAll()} will be needed to resume. Misfires on Triggers will be applied depending upon the Trigger misfire instructions
*/
@Override
public void pauseAll() throws JobSchedulerException {
try {
getScheduler().pauseAll();
triggerListeners(JobSchedulerEvent.pauseAllJobsEvent());
} catch (SchedulerException e) {
throw new JobSchedulerException(e);
}
}
/**
* Resume All Triggers that have been paused. Misfires on Triggers will be applied depending upon the Trigger misfire instructions
*/
@Override
public void resumeAll() throws JobSchedulerException {
try {
getScheduler().resumeAll();
triggerListeners(JobSchedulerEvent.resumeAllJobsEvent());
} catch (SchedulerException e) {
throw new JobSchedulerException(e);
}
}
public Map<String, Object> getMetaData() throws JobSchedulerException {
Map<String, Object> map = new HashMap<String, Object>();
try {
SchedulerMetaData metaData = getScheduler().getMetaData();
if (metaData != null) {
ObjectMapper objectMapper = new ObjectMapper();
map = objectMapper.convertValue(metaData, Map.class);
}
} catch (IllegalArgumentException | SchedulerException e) {
throw new JobSchedulerException(e);
}
return map;
}
public boolean jobExists(JobIdentifier jobIdentifier) {
Set<JobKey> jobKeys = null;
try {
jobKeys = getScheduler().getJobKeys(GroupMatcher.jobGroupEquals(jobIdentifier.getGroup()));
if (jobKeys != null && !jobKeys.isEmpty()) {
for (JobKey key : jobKeys) {
if (jobIdentifierForJobKey(key).equals(jobIdentifier)) {
return true;
}
}
}
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
return false;
}
public boolean triggerExists(TriggerIdentifier triggerIdentifier) {
Trigger trigger = null;
try {
trigger = getScheduler().getTrigger(triggerKeyForTriggerIdentifier(triggerIdentifier));
if (trigger != null) {
return true;
}
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
return false;
}
public SchedulerMetaData getSchedulerMetaData() throws SchedulerException {
return getScheduler().getMetaData();
}
public void scheduleJob(JobDetail jobDetail, Trigger cronTrigger) throws SchedulerException {
getScheduler().scheduleJob(jobDetail, cronTrigger);
}
public void scheduleJob(MethodInvokingJobDetailFactoryBean methodInvokingJobDetailFactoryBean,
CronTriggerFactoryBean cronTriggerFactoryBean) throws SchedulerException {
JobDetail job = methodInvokingJobDetailFactoryBean.getObject();
CronTrigger trigger = cronTriggerFactoryBean.getObject();
scheduleJob(job, trigger);
}
public void scheduleJob(JobDetail job, CronTriggerFactoryBean cronTriggerFactoryBean) throws SchedulerException {
CronTrigger trigger = cronTriggerFactoryBean.getObject();
scheduleJob(job, trigger);
}
public void scheduleJob(JobIdentifier jobIdentifier, TriggerIdentifier triggerIdentifier, Object obj, String targetMethod,
String cronExpression, Map<String, Object> jobData) throws SchedulerException {
MethodInvokingJobDetailFactoryBean jobDetailFactory = new MethodInvokingJobDetailFactoryBean();
jobDetailFactory.setTargetObject(obj);
jobDetailFactory.setTargetMethod(targetMethod);
jobDetailFactory.setName(jobIdentifier.getName());
jobDetailFactory.setGroup(jobIdentifier.getGroup());
CronTriggerFactoryBean triggerFactoryBean = new CronTriggerFactoryBean();
triggerFactoryBean.setCronExpression(cronExpression);
triggerFactoryBean.setJobDetail(jobDetailFactory.getObject());
triggerFactoryBean.setGroup(triggerIdentifier.getGroup());
triggerFactoryBean.setName(triggerIdentifier.getName());
scheduleJob(jobDetailFactory, triggerFactoryBean);
}
public void scheduleJob(JobIdentifier jobIdentifier, TriggerIdentifier triggerIdentifier, Class<? extends QuartzJobBean> clazz,
String cronExpression, Map<String, Object> jobData) throws SchedulerException {
scheduleJob(jobIdentifier, triggerIdentifier, clazz, cronExpression, jobData, false);
}
public void scheduleJob(JobIdentifier jobIdentifier, TriggerIdentifier triggerIdentifier, Class<? extends QuartzJobBean> clazz,
String cronExpression, Map<String, Object> jobData, boolean fireImmediately) throws SchedulerException {
if (jobData == null) {
jobData = new HashMap<>();
}
JobDataMap jobDataMap = new JobDataMap(jobData);
JobDetail job = newJob(clazz)
.withIdentity(jobIdentifier.getName(), jobIdentifier.getGroup())
.requestRecovery(false)
.setJobData(jobDataMap)
.build();
TriggerBuilder triggerBuilder = newTrigger()
.withIdentity(triggerIdentifier.getName(), triggerIdentifier.getGroup())
.withSchedule(cronSchedule(cronExpression).inTimeZone(TimeZone.getTimeZone("UTC"))
.withMisfireHandlingInstructionFireAndProceed())
.forJob(job.getKey());
if (fireImmediately) {
Date previousTriggerTime = null;
try {
previousTriggerTime = CronExpressionUtil.getPreviousFireTime(cronExpression);
if (previousTriggerTime != null) {
triggerBuilder.startAt(previousTriggerTime);
}
} catch (ParseException e) {
}
}
Trigger trigger = triggerBuilder.build();
scheduleJob(job, trigger);
}
public void scheduleJob(String groupName, String jobName, Class<? extends QuartzJobBean> clazz, String cronExpression,
Map<String, Object> jobData) throws SchedulerException {
scheduleJob(groupName, jobName, clazz, cronExpression, jobData, false);
}
public void scheduleJob(String groupName, String jobName, Class<? extends QuartzJobBean> clazz, String cronExpression,
Map<String, Object> jobData, boolean fireImmediately) throws SchedulerException {
scheduleJob(new DefaultJobIdentifier(jobName, groupName), new DefaultTriggerIdentifier(jobName, groupName), clazz,
cronExpression, jobData, fireImmediately);
}
public void updateTrigger(TriggerIdentifier triggerIdentifier, Trigger trigger) throws SchedulerException {
getScheduler().rescheduleJob(triggerKeyForTriggerIdentifier(triggerIdentifier), trigger);
}
@Override
public void subscribeToJobSchedulerEvents(JobSchedulerListener listener) {
listeners.add(listener);
}
private void triggerListeners(JobSchedulerEvent event) {
listeners.stream().forEach(listener -> listener.onJobSchedulerEvent(event));
}
}