/* * Sonatype Nexus (TM) Open Source Version * Copyright (c) 2008-present Sonatype, Inc. * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. * * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. * * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the * Eclipse Foundation. All other trademarks are the property of their respective owners. */ package org.sonatype.nexus.quartz.internal; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; import org.sonatype.nexus.common.app.ManagedLifecycle; import org.sonatype.nexus.common.event.EventHelper; import org.sonatype.nexus.common.event.EventManager; import org.sonatype.nexus.common.log.LastShutdownTimeService; import org.sonatype.nexus.common.node.NodeAccess; import org.sonatype.nexus.common.stateguard.Guarded; import org.sonatype.nexus.common.stateguard.StateGuardLifecycleSupport; import org.sonatype.nexus.common.text.Strings2; import org.sonatype.nexus.common.thread.TcclBlock; import org.sonatype.nexus.quartz.internal.task.QuartzTaskFuture; import org.sonatype.nexus.quartz.internal.task.QuartzTaskInfo; import org.sonatype.nexus.quartz.internal.task.QuartzTaskJob; import org.sonatype.nexus.quartz.internal.task.QuartzTaskJobListener; import org.sonatype.nexus.quartz.internal.task.QuartzTaskState; import org.sonatype.nexus.scheduling.CurrentState; import org.sonatype.nexus.scheduling.TaskConfiguration; import org.sonatype.nexus.scheduling.TaskInfo; import org.sonatype.nexus.scheduling.TaskRemovedException; import org.sonatype.nexus.scheduling.TaskState; import org.sonatype.nexus.scheduling.schedule.Manual; import org.sonatype.nexus.scheduling.schedule.Now; import org.sonatype.nexus.scheduling.schedule.Schedule; import org.sonatype.nexus.scheduling.schedule.ScheduleFactory; import org.sonatype.nexus.scheduling.spi.SchedulerSPI; import org.sonatype.nexus.thread.DatabaseStatusDelayedExecutor; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import org.quartz.JobBuilder; import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.quartz.JobKey; import org.quartz.JobPersistenceException; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SchedulerMetaData; import org.quartz.Trigger; import org.quartz.TriggerKey; import org.quartz.UnableToInterruptJobException; import org.quartz.core.QuartzScheduler; import org.quartz.spi.JobStore; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Maps.filterKeys; import static java.util.Collections.emptyMap; import static org.quartz.TriggerBuilder.newTrigger; import static org.quartz.TriggerKey.triggerKey; import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals; import static org.quartz.impl.matchers.KeyMatcher.keyEquals; import static org.sonatype.nexus.common.app.ManagedLifecycle.Phase.SERVICES; import static org.sonatype.nexus.common.stateguard.StateGuardLifecycleSupport.State.STARTED; import static org.sonatype.nexus.quartz.internal.task.QuartzTaskJobListener.listenerName; import static org.sonatype.nexus.quartz.internal.task.QuartzTaskUtils.configurationOf; import static org.sonatype.nexus.quartz.internal.task.QuartzTaskUtils.updateJobData; import static org.sonatype.nexus.scheduling.TaskConfiguration.LAST_RUN_STATE_END_STATE; import static org.sonatype.nexus.scheduling.TaskDescriptorSupport.LIMIT_NODE_KEY; import static org.sonatype.nexus.scheduling.TaskState.INTERRUPTED; import static org.sonatype.nexus.scheduling.TaskState.RUNNING; /** * Quartz {@link SchedulerSPI}. * * @since 3.0 */ @Named @ManagedLifecycle(phase = SERVICES) @Singleton public class QuartzSchedulerSPI extends StateGuardLifecycleSupport implements SchedulerSPI { public static final String MISSING_TRIGGER_RECOVERY = ".missingTriggerRecovery"; private static final String GROUP_NAME = "nexus"; private static final Set<String> INHERITED_CONFIG_KEYS = ImmutableSet.of(LIMIT_NODE_KEY); private final EventManager eventManager; private final NodeAccess nodeAccess; private final Provider<JobStore> jobStoreProvider; private final ScheduleFactory scheduleFactory; private final Provider<Scheduler> schedulerProvider; private final QuartzTriggerConverter triggerConverter; private final LastShutdownTimeService lastShutdownTimeService; private final DatabaseStatusDelayedExecutor delayedExecutor; private final boolean recoverInterruptedJobs; private Scheduler scheduler; private QuartzScheduler quartzScheduler; private boolean active; @SuppressWarnings("squid:S00107") //suppress constructor parameter count @Inject public QuartzSchedulerSPI(final EventManager eventManager, final NodeAccess nodeAccess, final Provider<JobStore> jobStoreProvider, final Provider<Scheduler> schedulerProvider, final LastShutdownTimeService lastShutdownTimeService, final DatabaseStatusDelayedExecutor delayedExecutor, @Named("${nexus.quartz.recoverInterruptedJobs:-true}") final boolean recoverInterruptedJobs) { this.eventManager = checkNotNull(eventManager); this.nodeAccess = checkNotNull(nodeAccess); this.jobStoreProvider = checkNotNull(jobStoreProvider); this.schedulerProvider = checkNotNull(schedulerProvider); this.lastShutdownTimeService = checkNotNull(lastShutdownTimeService); this.recoverInterruptedJobs = recoverInterruptedJobs; this.delayedExecutor = checkNotNull(delayedExecutor); this.scheduleFactory = new QuartzScheduleFactory(); this.triggerConverter = new QuartzTriggerConverter(this.scheduleFactory); // FIXME: sort out with refinement to lifecycle below this.active = true; } public QuartzTriggerConverter triggerConverter() { return triggerConverter; } @VisibleForTesting Scheduler getScheduler() { return scheduler; } // // Lifecycle // @Override protected void doStart() throws Exception { // create new scheduler scheduler = schedulerProvider.get(); try { // access internal scheduler to simulate signals for remote updates Field schedField = scheduler.getClass().getDeclaredField("sched"); schedField.setAccessible(true); quartzScheduler = (QuartzScheduler) schedField.get(scheduler); } catch (Exception | LinkageError e) { log.error("Cannot find QuartzScheduler", e); throw e; } // re-attach listeners right after scheduler is available reattachListeners(); } private void reattachListeners() { final Optional<Date> lastShutdownTime = lastShutdownTimeService.estimateLastShutdownTime(); forEachNexusJob((final Trigger trigger, final JobDetail jobDetail) -> { try { updateLastRunStateInfo(jobDetail, lastShutdownTime); } catch (SchedulerException e) { log.error("Error updating last run state for {}", jobDetail.getKey(), e); } }); forEachNexusJob((final Trigger trigger, final JobDetail jobDetail) -> { try { stubJobListener(jobDetail); } catch (SchedulerException e) { log.error("Error attaching job listener to {}", jobDetail.getKey(), e); } }); delayedExecutor.execute(() -> { forEachNexusJob((final Trigger trigger, final JobDetail jobDetail) -> { try { attachJobListener(jobDetail, trigger); } catch (SchedulerException e) { log.error("Error attaching job listener to {}", jobDetail.getKey(), e); } }); if (recoverInterruptedJobs) { forEachNexusJob(this::recoverJob); } }); } private void forEachNexusJob(final BiConsumer<Trigger, JobDetail> consumer) { try { for (Entry<Trigger, JobDetail> entry : getNexusJobs().entrySet()) { consumer.accept(entry.getKey(), entry.getValue()); } } catch (SchedulerException e) { log.error("Error getting jobs to process", e); } } @VisibleForTesting void recoverJob(final Trigger trigger, final JobDetail jobDetail) { if (shouldRecoverJob(trigger, jobDetail)) { try { Trigger newTrigger = newTrigger() .usingJobData(trigger.getJobDataMap()) .withDescription("Recovery of " + trigger.getDescription()) .forJob(jobDetail) .startNow() .build(); log.info("Recovering job {}", newTrigger.getJobKey()); scheduler.scheduleJob(newTrigger); } catch (SchedulerException e) { log.error("Failed to recover job {}", trigger.getJobKey(), e); } } } private static Boolean shouldRecoverJob(final Trigger trigger, final JobDetail jobDetail) { return (jobDetail.requestsRecovery() && isInterruptedJob(jobDetail)) || isRunNow(trigger); } /** * Checks the last run time against its last trigger fire time. * If the trigger's last fire time doesn't match with the jobs last fire time, * then the {@link TaskState} is set to interrupted * * @param nexusLastRunTime - approximate time at which the last instance of nexus was shutdown */ private void updateLastRunStateInfo(final JobDetail jobDetail, final Optional<Date> nexusLastRunTime) throws SchedulerException { Optional<Date> latestFireWrapper = scheduler.getTriggersOfJob(jobDetail.getKey()).stream() .filter(Objects::nonNull) .map(Trigger::getPreviousFireTime) .filter(Objects::nonNull) .max(Date::compareTo); if (latestFireWrapper.isPresent()) { TaskConfiguration taskConfig = configurationOf(jobDetail); Date latestFire = latestFireWrapper.get(); if (!taskConfig.hasLastRunState() || taskConfig.getLastRunState().getRunStarted().before(latestFire)) { long estimatedDuration = Math.max(nexusLastRunTime.orElse(latestFire).getTime() - latestFire.getTime(), 0); taskConfig.setLastRunState(INTERRUPTED, latestFire, estimatedDuration); log.warn("Updating lastRunState to interrupted for jobKey {} taskConfig: {}", jobDetail.getKey(), taskConfig); try { updateJobData(jobDetail, taskConfig); scheduler.addJob(jobDetail, true, true); } catch (RuntimeException e) { log.warn("Problem updating lastRunState to interrupted for jobKey {}", jobDetail.getKey(), e); } } } } @Override protected void doStop() throws Exception { scheduler = null; } // TODO: Simplify active/pause/resume bits here @Override public void pause() { try { setActive(false); } catch (SchedulerException e) { throw new RuntimeException(e); } } @Override public void resume() { try { setActive(true); } catch (SchedulerException e) { throw new RuntimeException(e); } } private void setActive(final boolean started) throws SchedulerException { this.active = started; if (isStarted()) { applyActive(); } } private void applyActive() throws SchedulerException { try (TcclBlock tccl = TcclBlock.begin(this)) { if (!active && !scheduler.isInStandbyMode()) { scheduler.standby(); log.info("Scheduler put into stand-by mode"); } else if (active && scheduler.isInStandbyMode()) { scheduler.start(); log.info("Scheduler put into ready mode"); } } } // // JobListeners // /** * Schedules a manually executable trigger for a job missing a trigger and adds marker for health check reporting */ private Trigger scheduleJobWithManualTrigger(final JobKey jobKey, final JobDetail jobDetail, final TriggerKey triggerKey) throws SchedulerException { log.error("Missing trigger for key: {}", jobKey); Trigger trigger = triggerConverter.convert(new Manual()) .usingJobData(jobDetail.getJobDataMap()) .usingJobData(MISSING_TRIGGER_RECOVERY, jobKey.getName()) .withIdentity(triggerKey) .withDescription(jobDetail.getDescription()) .forJob(jobDetail) .build(); log.info("Rescheduling job '{}' with manual trigger", jobDetail.getDescription()); scheduler.scheduleJob(trigger); return trigger; } /** * Attach {@link QuartzTaskJobListener} to job. */ private QuartzTaskJobListener attachJobListener(final JobDetail jobDetail, final Trigger trigger) throws SchedulerException { log.debug("Initializing task-state: jobDetail={}, trigger={}", jobDetail, trigger); Date now = new Date(); TaskConfiguration taskConfiguration = configurationOf(jobDetail); Schedule schedule = triggerConverter.convert(trigger); QuartzTaskState taskState = new QuartzTaskState( taskConfiguration, schedule, trigger.getFireTimeAfter(now) ); QuartzTaskFuture future = null; if (schedule instanceof Now) { future = new QuartzTaskFuture( this, jobDetail.getKey(), taskConfiguration.getTaskLogName(), now, schedule, null ); } QuartzTaskJobListener listener = new QuartzTaskJobListener( listenerName(jobDetail.getKey()), eventManager, this, new QuartzTaskInfo(eventManager, this, jobDetail.getKey(), taskState, future) ); scheduler.getListenerManager().addJobListener(listener, keyEquals(jobDetail.getKey())); return listener; } /** * Creates a stub of a {@link QuartzTaskJobListener} attached to the job with scheduling unset. * See NEXUS-18983 */ private QuartzTaskJobListener stubJobListener(final JobDetail jobDetail) throws SchedulerException { log.debug("Stubbing task-state: jobDetail={}", jobDetail); TaskConfiguration taskConfiguration = configurationOf(jobDetail); Schedule schedule = scheduleFactory.manual(); QuartzTaskState taskState = new QuartzTaskState(taskConfiguration, schedule, null); QuartzTaskJobListener listener = new QuartzTaskJobListener(listenerName(jobDetail.getKey()), eventManager, this, new QuartzTaskInfo(eventManager, this, jobDetail.getKey(), taskState, null)); scheduler.getListenerManager().addJobListener(listener, keyEquals(jobDetail.getKey())); return listener; } /** * Returns listener for given job, or null if not found. */ @Nullable private QuartzTaskJobListener findJobListener(final JobKey jobKey) throws SchedulerException { String name = listenerName(jobKey); return (QuartzTaskJobListener) scheduler.getListenerManager().getJobListener(name); } private void updateJobListener(final JobDetail jobDetail) throws SchedulerException { QuartzTaskJobListener toBeUpdated = findJobListener(jobDetail.getKey()); if (toBeUpdated != null) { QuartzTaskInfo taskInfo = toBeUpdated.getTaskInfo(); taskInfo.setNexusTaskStateIfWaiting( new QuartzTaskState( taskInfo.getConfiguration().apply(configurationOf(jobDetail)), taskInfo.getSchedule(), taskInfo.getCurrentState().getNextRun() ), taskInfo.getTaskFuture() ); } } private void updateJobListener(final Trigger trigger) throws SchedulerException { QuartzTaskJobListener toBeUpdated = findJobListener(trigger.getJobKey()); if (toBeUpdated != null) { QuartzTaskInfo taskInfo = toBeUpdated.getTaskInfo(); taskInfo.setNexusTaskStateIfWaiting( new QuartzTaskState( taskInfo.getConfiguration(), triggerConverter.convert(trigger), trigger.getFireTimeAfter(new Date()) ), taskInfo.getTaskFuture() ); } } private void removeJobListener(final JobKey jobKey) throws SchedulerException { String name = listenerName(jobKey); QuartzTaskJobListener toBeRemoved = (QuartzTaskJobListener) scheduler.getListenerManager().getJobListener(name); if (toBeRemoved != null) { // ensure future is done QuartzTaskFuture future = toBeRemoved.getTaskInfo().getTaskFuture(); if (future != null && !future.isDone()) { future.doCancel(); } scheduler.getListenerManager().removeJobListener(name); } } // // SchedulerSPI // @Override @Guarded(by = STARTED) public ScheduleFactory scheduleFactory() { return scheduleFactory; } @Override @Guarded(by = STARTED) public String renderStatusMessage() { StringBuilder buff = new StringBuilder(); SchedulerMetaData metaData; try { metaData = scheduler.getMetaData(); } catch (SchedulerException e) { throw new RuntimeException(e); } if (metaData.isShutdown()) { buff.append("Shutdown"); } else { if (metaData.getRunningSince() != null) { buff.append("Started"); } else { buff.append("Stopped"); } if (metaData.isInStandbyMode()) { buff.append("; Stand-by"); } } return buff.toString(); } @Override @Guarded(by = STARTED) public String renderDetailMessage() { try { return scheduler.getMetaData().getSummary(); } catch (SchedulerException e) { throw new RuntimeException(e); } } @Override @Nullable @Guarded(by = STARTED) public TaskInfo getTaskById(final String id) { try { QuartzTaskInfo task = findTaskById(id); if (task != null && !task.isRemovedOrDone()) { return task; } } catch (IllegalStateException e) { // no listener found in taskByKey, means no job exists } catch (SchedulerException e) { throw new RuntimeException(e); } return null; } @Override @Guarded(by = STARTED) public List<TaskInfo> listsTasks() { try { // returns all tasks which are NOT removed or done return allTasks().values().stream() .filter((task) -> !task.isRemovedOrDone()) .collect(Collectors.toList()); } catch (SchedulerException e) { throw new RuntimeException(e); } } @Override @Guarded(by = STARTED) public List<String> getMissingTriggerDescriptions() { try { try (TcclBlock tccl = TcclBlock.begin(this)) { Set<JobKey> jobKeys = scheduler.getJobKeys(jobGroupEquals(GROUP_NAME)); List<String> missingJobDescriptions = new ArrayList<>(); for (JobKey jobKey : jobKeys) { Trigger trigger = scheduler.getTrigger(triggerKey(jobKey.getName(), jobKey.getGroup())); if (trigger.getJobDataMap().containsKey(MISSING_TRIGGER_RECOVERY)) { missingJobDescriptions.add(trigger.getDescription()); } } return missingJobDescriptions; } } catch (SchedulerException e) { throw new RuntimeException(e); } } @Override @Guarded(by = STARTED) public TaskInfo scheduleTask(final TaskConfiguration config, final Schedule schedule) { checkState(!EventHelper.isReplicating(), "Replication in progress"); try (TcclBlock tccl = TcclBlock.begin(this)) { // check for existing task with same id QuartzTaskInfo old = findTaskById(config.getId()); if (old != null) { checkState(!(old.getSchedule() instanceof Now), "Run 'now' task cannot be rescheduled"); checkState(!old.isRemovedOrDone(), "Done task cannot be rescheduled"); QuartzTaskFuture future = old.getTaskFuture(); if (future != null) { // is running checkState(!(schedule instanceof Now), "Running task cannot be rescheduled with 'now'"); } log.debug("Task {} : {} rescheduled {} -> {} ", old.getJobKey().getName(), old.getConfiguration().getTaskLogName(), old.getSchedule(), schedule ); JobDetail jobDetail = buildJob(config, old.getJobKey()); Trigger trigger = buildTrigger(schedule, jobDetail); scheduler.addJob(jobDetail, true, true); scheduler.rescheduleJob(trigger.getKey(), trigger); // update TaskInfo, but only if it's WAITING, as running one will pick up the change by job listener when done old.setNexusTaskStateIfWaiting( new QuartzTaskState( config, schedule, trigger.getFireTimeAfter(new Date()) ), future ); if (!config.isEnabled()) { scheduler.pauseJob(old.getJobKey()); } else { scheduler.resumeJob(old.getJobKey()); } return old; } else { // Use always new jobKey, as if THIS task reschedules THIS/itself, "new" should not interfere with "this" // Currently only healthcheck does this, by rescheduling itself JobKey jobKey = JobKey.jobKey(UUID.randomUUID().toString(), GROUP_NAME); // get trigger, but use identity of jobKey // This is only for simplicity, as is not a requirement: NX job:triggers are 1:1 so tying them as this is ok // ! create the trigger before eventual TaskInfo remove bellow to avoid task removal in case of an invalid trigger JobDetail jobDetail = buildJob(config, jobKey); Trigger trigger = buildTrigger(schedule, jobDetail); log.debug("Task {} : {} scheduled with key: {} and schedule: {}", config.getId(), config.getTaskLogName(), jobKey.getName(), schedule ); // register job specific listener with initial state QuartzTaskJobListener listener = attachJobListener(jobDetail, trigger); scheduler.scheduleJob(jobDetail, trigger); if (!config.isEnabled()) { scheduler.pauseJob(jobKey); } return listener.getTaskInfo(); } } catch (SchedulerException e) { throw new RuntimeException(e); } } private JobDetail buildJob(final TaskConfiguration config, final JobKey jobKey) { return JobBuilder.newJob(QuartzTaskJob.class) .withIdentity(jobKey) .withDescription(config.getName()) .requestRecovery(config.isRecoverable()) .usingJobData(new JobDataMap(config.asMap())) .build(); } private Trigger buildTrigger(final Schedule schedule, final JobDetail jobDetail) { return ensureStartsInTheFuture(triggerConverter.convert(schedule) .withIdentity(jobDetail.getKey().getName(), jobDetail.getKey().getGroup()) .withDescription(jobDetail.getDescription()) .usingJobData(new JobDataMap(filterKeys(jobDetail.getJobDataMap(), INHERITED_CONFIG_KEYS::contains))) .build()); } @Override @Guarded(by = STARTED) public int getRunningTaskCount() { try (TcclBlock tccl = TcclBlock.begin(this)) { return scheduler.getCurrentlyExecutingJobs().size(); } catch (SchedulerException e) { throw new RuntimeException(e); } } @Override @Guarded(by = STARTED) public int getExecutedTaskCount() { return quartzScheduler.numJobsExecuted(); } // // Internal // /** * Ensure that trigger start date is not in the past. */ private Trigger ensureStartsInTheFuture(final Trigger trigger) { Date now = new Date(); if (trigger.getStartTime().before(now)) { Date fireTimeAfter = trigger.getFireTimeAfter(now); if (fireTimeAfter != null) { return trigger.getTriggerBuilder().startAt(fireTimeAfter).build(); } } return trigger; } /** * Returns all tasks for the {@link #GROUP_NAME} group, which also have attached job-listeners. */ private Map<JobKey, QuartzTaskInfo> allTasks() throws SchedulerException { try (TcclBlock tccl = TcclBlock.begin(this)) { Map<JobKey, QuartzTaskInfo> result = new HashMap<>(); Set<JobKey> jobKeys = scheduler.getJobKeys(jobGroupEquals(GROUP_NAME)); for (JobKey jobKey : jobKeys) { QuartzTaskJobListener listener = findJobListener(jobKey); if (listener != null) { result.put(jobKey, listener.getTaskInfo()); } else { // TODO: Sort out if this is normal or edge-case indicative of a bug or not log.debug("Job missing listener; omitting from results: {}", jobKey); } } return result; } } private Map<Trigger, JobDetail> getNexusJobs() throws SchedulerException { Map<Trigger, JobDetail> nexusJobs = new HashMap<>(); try (TcclBlock tccl = TcclBlock.begin(this)) { Set<JobKey> jobKeys = scheduler.getJobKeys(jobGroupEquals(GROUP_NAME)); for (JobKey jobKey : jobKeys) { JobDetail jobDetail = scheduler.getJobDetail(jobKey); if (jobDetail == null) { log.error("Missing job-detail for key: {}", jobKey); continue; } TriggerKey triggerKey = triggerKey(jobKey.getName(), jobKey.getGroup()); Trigger trigger = scheduler.getTrigger(triggerKey); if (trigger == null) { trigger = scheduleJobWithManualTrigger(jobKey, jobDetail, triggerKey); } nexusJobs.put(trigger, jobDetail); } } return nexusJobs; } /** * Returns task-info for given identifier, or null. */ @Nullable private QuartzTaskInfo findTaskById(final String id) throws SchedulerException { try (TcclBlock tccl = TcclBlock.begin(this)) { return allTasks().values().stream() .filter((task) -> task.getId().equals(id)) .findFirst() .orElse(null); } } /** * Used by {@link QuartzTaskFuture#cancel(boolean)}. */ @Guarded(by = STARTED) public boolean cancelJob(final JobKey jobKey) { checkNotNull(jobKey); try (TcclBlock tccl = TcclBlock.begin(this)) { return scheduler.interrupt(jobKey); } catch (UnableToInterruptJobException e) { log.debug("Unable to interrupt job with key: {}", jobKey, e); } return false; } /** * Used by {@link QuartzTaskInfo#runNow()}. */ @Guarded(by = STARTED) public void runNow(final String triggerSource, final JobKey jobKey, final QuartzTaskInfo taskInfo, final QuartzTaskState taskState) throws TaskRemovedException, SchedulerException { checkState(active, "Cannot run tasks while scheduler is paused"); TaskConfiguration config = taskState.getConfiguration(); // avoid marking local state as running if task is limited to run on a different node if (!isLimitedToAnotherNode(config)) { taskInfo.setNexusTaskState( RUNNING, taskState, new QuartzTaskFuture( this, jobKey, config.getTaskLogName(), new Date(), scheduleFactory().now(), triggerSource ) ); } try (TcclBlock tccl = TcclBlock.begin(this)) { // triggering with dataMap from "now" trigger as it contains metadata for back-conversion in listener JobDataMap triggerDetail = triggerConverter.convert(scheduleFactory().now()).build().getJobDataMap(); triggerDetail.putAll(filterKeys(config.asMap(), INHERITED_CONFIG_KEYS::contains)); scheduler.triggerJob(jobKey, triggerDetail); } catch (JobPersistenceException e) { throw new TaskRemovedException(jobKey.getName(), e); } } /** * Used by {@link QuartzTaskInfo#remove()}. */ @Guarded(by = STARTED) public boolean removeTask(final JobKey jobKey) { try (TcclBlock tccl = TcclBlock.begin(this)) { boolean result = false; List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey); for (Trigger trigger : triggers) { result = scheduler.unscheduleJob(trigger.getKey()) || result; } removeJobListener(jobKey); return result; } catch (SchedulerException e) { throw new RuntimeException(e); } } @Guarded(by = STARTED) @Override public boolean cancel(final String id, final boolean mayInterruptIfRunning) { return Optional.ofNullable(id) .map(this::getTaskById) .map(TaskInfo::getCurrentState) .map(CurrentState::getFuture) .map(f -> f.cancel(mayInterruptIfRunning)) .orElse(false); } @Nullable @Override public TaskInfo getTaskByTypeId(final String typeId) { return getTaskByTypeId(typeId, emptyMap()); } @Nullable @Override public TaskInfo getTaskByTypeId(final String typeId, final Map<String, String> config) { checkNotNull(typeId); checkNotNull(config); return listsTasks().stream() .filter(t -> typeId.equals(t.getTypeId())) .filter(matchConfig(config)) .findFirst() .orElse(null); } @Override public boolean findAndSubmit(final String typeId) { return findAndSubmit(typeId, emptyMap()); } @Override public boolean findAndSubmit(final String typeId, final Map<String, String> config) { checkNotNull(typeId); checkNotNull(config); TaskInfo taskInfo = getTaskByTypeId(typeId, config); if (taskInfo == null) { return false; } else { try { if (!taskInfo.getCurrentState().getState().isRunning()) { taskInfo.runNow(); } } catch (TaskRemovedException e) { log.error("Unable to submit task: {}", taskInfo, e); } return true; } } private Predicate<TaskInfo> matchConfig(final Map<String, String> config) { return t -> { TaskConfiguration tc = t.getConfiguration(); return config.entrySet().stream() .filter(e -> e.getKey() != null) .filter(e -> e.getValue() != null) .allMatch(e -> e.getValue().equals(tc.getString(e.getKey()))); }; } public void remoteJobCreated(JobDetail jobDetail) { // simulate signals Quartz would have sent quartzScheduler.getSchedulerSignaler().signalSchedulingChange(0L); quartzScheduler.notifySchedulerListenersJobAdded(jobDetail); } public void remoteJobUpdated(JobDetail jobDetail) throws SchedulerException { updateJobListener(jobDetail); // simulate signals Quartz would have sent quartzScheduler.getSchedulerSignaler().signalSchedulingChange(0L); quartzScheduler.notifySchedulerListenersJobAdded(jobDetail); } public void remoteJobDeleted(JobDetail jobDetail) throws SchedulerException { // simulate signals Quartz would have sent quartzScheduler.getSchedulerSignaler().signalSchedulingChange(0L); quartzScheduler.notifySchedulerListenersJobDeleted(jobDetail.getKey()); removeJobListener(jobDetail.getKey()); } public void remoteTriggerCreated(Trigger trigger) throws SchedulerException { if (!isRunNow(trigger)) { attachJobListener(jobStoreProvider.get().retrieveJob(trigger.getJobKey()), trigger); // simulate signals Quartz would have sent quartzScheduler.getSchedulerSignaler().signalSchedulingChange(getNextFireMillis(trigger)); quartzScheduler.notifySchedulerListenersSchduled(trigger); } else if (isLimitedToThisNode(trigger)) { // special "run-now" task which was created on a different node to where it will run // when this happens we ping the scheduler to make sure it runs as soon as possible quartzScheduler.getSchedulerSignaler().signalSchedulingChange(0L); quartzScheduler.notifySchedulerListenersSchduled(trigger); } } public void remoteTriggerUpdated(Trigger trigger) throws SchedulerException { if (!isRunNow(trigger)) { updateJobListener(trigger); // simulate signals Quartz would have sent quartzScheduler.getSchedulerSignaler().signalSchedulingChange(getNextFireMillis(trigger)); quartzScheduler.notifySchedulerListenersUnscheduled(trigger.getKey()); quartzScheduler.notifySchedulerListenersSchduled(trigger); } } public void remoteTriggerDeleted(Trigger trigger) throws SchedulerException { if (!isRunNow(trigger)) { // simulate signals Quartz would have sent quartzScheduler.getSchedulerSignaler().signalSchedulingChange(0L); quartzScheduler.notifySchedulerListenersUnscheduled(trigger.getKey()); removeJobListener(trigger.getJobKey()); } } /** * See {@link QuartzTaskInfo#runNow(String)} * * @since 3.2 */ public boolean isLimitedToAnotherNode(final TaskConfiguration config) { if (nodeAccess.isClustered() && config.containsKey(LIMIT_NODE_KEY)) { String limitedNodeId = config.getString(LIMIT_NODE_KEY); checkState(!Strings2.isBlank(limitedNodeId), "Task '%s' is not configured for HA", config.getName()); checkState(nodeAccess.getMemberIds().contains(limitedNodeId), "Task '%s' uses node %s which is not a member of this cluster", config.getName(), limitedNodeId); return !nodeAccess.getId().equals(limitedNodeId); } return false; } private boolean isLimitedToThisNode(final Trigger trigger) { // can skip isClustered check because this method is only called when in HA mode return nodeAccess.getId().equals(trigger.getJobDataMap().getString(LIMIT_NODE_KEY)); } private static boolean isRunNow(final Trigger trigger) { return Now.TYPE.equals(trigger.getJobDataMap().getString(Schedule.SCHEDULE_TYPE)); } private static boolean isInterruptedJob(final JobDetail jobDetail) { return INTERRUPTED.name().equals(jobDetail.getJobDataMap().getString(LAST_RUN_STATE_END_STATE)); } private static long getNextFireMillis(final Trigger trigger) { Date nextFireTime = trigger.getNextFireTime(); return nextFireTime != null ? nextFireTime.getTime() : 0L; } }