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)); } }