/** * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2012-2015 ForgeRock AS. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * */ package org.forgerock.openidm.quartz.impl; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; import org.forgerock.openidm.router.IDMConnectionFactory; import org.forgerock.services.context.Context; import org.forgerock.json.JsonValue; import org.forgerock.json.JsonValueException; import org.forgerock.json.resource.ConnectionFactory; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.DeleteRequest; import org.forgerock.json.resource.NotFoundException; import org.forgerock.json.resource.PreconditionFailedException; import org.forgerock.json.resource.Requests; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.openidm.cluster.ClusterEvent; import org.forgerock.openidm.cluster.ClusterEventListener; import org.forgerock.openidm.cluster.ClusterManagementService; import org.forgerock.openidm.core.IdentityServer; import org.forgerock.openidm.util.ContextUtil; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.quartz.Calendar; import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.quartz.JobPersistenceException; import org.quartz.ObjectAlreadyExistsException; import org.quartz.SchedulerException; import org.quartz.Trigger; import org.quartz.core.SchedulingContext; import org.quartz.spi.ClassLoadHelper; import org.quartz.spi.JobStore; import org.quartz.spi.SchedulerSignaler; import org.quartz.spi.TriggerFiredBundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A JobStore implementation used for persistence with the OpenIdm Repository Service. */ public class RepoJobStore implements JobStore, ClusterEventListener { private final static Logger logger = LoggerFactory.getLogger(RepoJobStore.class); private final static Object lock = new Object(); /** * An identifier used to create unique keys for Jobs and Triggers. */ private final static String UNIQUE_ID_SEPARATOR = "_$x$x$_"; /** * Cluster Management Service */ private ClusterManagementService clusterManager; /** * The SchedulerSignaler to send notification to */ private SchedulerSignaler schedulerSignaler; /** * The ClassLoadHelper (currently unused) */ private ClassLoadHelper loadHelper; /** * The instance ID */ private String instanceId; /** * The listener ID used for registration with the Cluster Management Service */ private String listenerId = "scheduler"; /** * The instance Name (currently unused) */ private String instanceName; /** * The misfire threshold */ private long misfireThreshold = 10000; /** * Number of retries on failed writes to the repository (defaults to -1, infinite). */ private int writeRetries = -1; /** * A list of all "blocked" jobs. */ private List<String> blockedJobs = new ArrayList<String>(); /** * An AtomicLong used for creating record IDs */ private static AtomicLong ftrCtr = new AtomicLong(System.currentTimeMillis()); /** * The Repository Service Accessor */ private static Context context = null; /** * Boolean indicating if the scheduler has called shutdown() */ private volatile boolean shutdown = false; /** * Creates a new <code>RepoJobStore</code>. */ public RepoJobStore() { } /** * <p> * Called by the QuartzScheduler before the <code>JobStore</code> is * used, in order to give the it a chance to initialize. * </p> */ @Override public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler schedSignaler) { logger.debug("Initializing RepoJobStore"); this.schedulerSignaler = schedSignaler; this.loadHelper = loadHelper; // Set the number of retries for failed writes to the repository this.writeRetries = Integer.parseInt(IdentityServer.getInstance().getProperty("openidm.scheduler.repo.retry", "-1")); cleanUpInstance(); } /** * Sets the Repository Service Router and returns true if successful, false otherwise. * * @return true if successful, false otherwise */ public Context getContext() throws JobPersistenceException { if (context == null) { context = ContextUtil.createInternalContext(); } return context; } public static void setContext(Context context) { RepoJobStore.context = context; } public boolean setClusterService() { if (clusterManager == null) { BundleContext ctx = FrameworkUtil.getBundle(RepoJobStore.class).getBundleContext(); ServiceReference serviceReference = ctx.getServiceReference(ClusterManagementService.class.getName()); clusterManager = ClusterManagementService.class.cast(ctx.getService(serviceReference)); return !(clusterManager == null); } return true; } private ConnectionFactory connectionFactory; /** * Sets the ConnectionFactory and returns true if successful, false otherwise. * * @return true if successful, false otherwise */ public ConnectionFactory getConnectionFactory() throws JobPersistenceException { if (connectionFactory == null) { BundleContext ctx = FrameworkUtil.getBundle(IDMConnectionFactory.class).getBundleContext(); ServiceReference serviceReference = null; try { serviceReference = ctx.getServiceReferences(IDMConnectionFactory.class, "(openidm.router.prefix=/)").iterator().next(); } catch (InvalidSyntaxException e) { /* ignore the filter is correct */ } connectionFactory = IDMConnectionFactory.class.cast(ctx.getService(serviceReference)); if (connectionFactory == null) { throw new JobPersistenceException("Connection factory is null"); } } return connectionFactory; } @Override public void schedulerStarted() throws SchedulerException { logger.debug("Job Scheduler Started"); if (isClustered()) { if (isClustered()) { if (setClusterService()) { logger.info("Registering with ClusterManagementService"); clusterManager.register(listenerId, this); } else { logger.error("ClusterManagementService not available"); } } } } /** * Gets the repository ID prefix * * @return the repository ID prefix */ private String getIdPrefix() { return "/repo/scheduler/"; } /** * Gets the Calendar's repository ID. * * @param id the Calendar's ID * @return the repository ID */ private String getCalendarsRepoId(String id) { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("calendars/").append(id).toString(); } /** * Gets the Calendar names repository ID. * * @return the repository ID */ private String getCalendarNamesRepoId() { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("calendarNames").toString(); } /** * Gets the Trigger's repository ID. * * @param group the Trigger's group * @param name the Trigger's name * @return the repository ID */ private String getTriggersRepoId(String group, String name) { return new StringBuilder(getIdPrefix()).append("triggers/") .append(getTriggerId(group, name)).toString(); } /** * Gets the Trigger groups repository ID. * * @return the repository ID */ private String getTriggerGroupsRepoId(String groupName) { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("triggerGroups/") .append(groupName).toString(); } /** * Gets the Trigger group names repository ID. * * @return the repository ID */ private String getTriggerGroupNamesRepoId() { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("triggerGroupNames").toString(); } /** * Gets the paused Trigger group names repository ID. * * @return the repository ID */ private String getPausedTriggerGroupNamesRepoId() { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("triggerPausedGroupNames").toString(); } /** * Gets the Job's repository ID. * * @param group the Job's group * @param name the Job's name * @return the repository ID */ private String getJobsRepoId(String group, String name) { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("jobs/").append(getJobId(group, name)).toString(); } /** * Gets the Job groups repository ID. * * @param id the group name * @return the repository ID */ private String getJobGroupsRepoId(String groupName) { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("jobGroups/").append(groupName).toString(); } /** * Gets the Job group names repository ID. * * @return the repository ID */ private String getJobGroupNamesRepoId() { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("jobGroupNames").toString(); } /** * Gets the paused Job groups names repository ID. * * @return the repository ID */ private String getPausedJobGroupNamesRepoId() { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("jobPausedGroupNames").toString(); } /** * Gets the Waiting Triggers repository ID. * * @return the repository ID */ private String getWaitingTriggersRepoId() { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("waitingTriggers").toString(); } /** * Gets the Acquired Triggers repository ID. * * @return the repository ID */ private String getAcquiredTriggersRepoId() { StringBuilder sb = new StringBuilder(); return sb.append(getIdPrefix()).append("acquiredTriggers").toString(); } /** * Gets the Trigger ID. * * @param group the Trigger group * @param name the Trigger name * @return the Trigger ID */ private String getTriggerId(String group, String name) { return new StringBuilder(group).append(UNIQUE_ID_SEPARATOR) .append(name).toString(); } /** * Gets the Job ID. * * @param group the Job group * @param name the Job name * @return the Job ID */ private String getJobId(String group, String name) { return new StringBuilder(group).append(UNIQUE_ID_SEPARATOR) .append(name).toString(); } private String getGroupFromId(String id) { return id.substring(0, id.indexOf(UNIQUE_ID_SEPARATOR)); } private String getNameFromId(String id) { return id.substring(id.indexOf(UNIQUE_ID_SEPARATOR) + UNIQUE_ID_SEPARATOR.length()); } /** * <p> * Called by the QuartzScheduler to inform the <code>JobStore</code> that * it should free up all of it's resources because the scheduler is * shutting down. * </p> */ @Override public void shutdown() { synchronized(lock) { shutdown = true; logger.debug("Job Scheduler Stopped"); } } @Override public boolean supportsPersistence() { return true; } @Override public void setInstanceId(String instanceId) { this.instanceId = instanceId; } @Override public void setInstanceName(String instanceName) { this.instanceName = instanceName; } @Override public void storeCalendar(SchedulingContext context, String name, Calendar calendar, boolean replaceExisting, boolean updateTriggers) throws ObjectAlreadyExistsException, JobPersistenceException { synchronized (lock) { try { CalendarWrapper cw = new CalendarWrapper(calendar, name); if (retrieveCalendar(context, name) == null) { // Create Calendar logger.debug("Creating Calendar: {}", name); getConnectionFactory().getConnection().create(getContext(), getCreateRequest(getCalendarsRepoId(name), cw.getValue().asMap())); } else { if (!replaceExisting) { throw new ObjectAlreadyExistsException(name); } // Update Calendar CalendarWrapper oldCw = getCalendarWrapper(name); logger.debug("Updating Calendar: {}", name); UpdateRequest r = Requests.newUpdateRequest(getCalendarsRepoId(name), cw.getValue()); r.setRevision(oldCw.getRevision()); getConnectionFactory().getConnection().update(getContext(), r); } if (updateTriggers) { List<TriggerWrapper> twList = getTriggerWrappersForCalendar(name); for (TriggerWrapper tw : twList) { Trigger t = tw.getTrigger(); boolean removed = removeWaitingTrigger(t); t.updateWithNewCalendar(calendar, getMisfireThreshold()); tw.updateTrigger(t); logger.debug("Updating Trigger {} in group {}", tw.getName(),tw.getGroup()); updateTriggerInRepo(tw.getGroup(), tw.getName(), tw, tw.getRevision()); if (removed) { addWaitingTrigger(t); } } } } catch (Exception e) { logger.warn("Error storing calendar: {}", name, e); throw new JobPersistenceException("Error storing calendar", e); } } } @Override public void storeJob(SchedulingContext context, JobDetail newJob, boolean replaceExisting) throws ObjectAlreadyExistsException, JobPersistenceException { synchronized (lock) { logger.debug("Attempting to store job {} ", newJob.getFullName()); String jobName = newJob.getName(); String jobGroup = newJob.getGroup(); String jobId = getJobsRepoId(jobGroup, jobName); JobGroupWrapper jgw = null; try { // Get job group jgw = getOrCreateJobGroupWrapper(jobGroup); } catch (ResourceException e) { logger.warn("Error storing job", e); throw new JobPersistenceException("Error" + " storing job", e); } List<String> jobNames = jgw.getJobNames(); JobWrapper jw = new JobWrapper(newJob, jgw.isPaused()); if (jobNames.contains(jobName) && !replaceExisting) { throw new ObjectAlreadyExistsException(newJob); } try { // Check if job name exists if (jobNames.contains(jobName)) { // Update job JobWrapper oldJw = getJobWrapper(jobGroup, jobName); logger.debug("Updating Job {} in group {}", jobName, jobGroup); UpdateRequest r = Requests.newUpdateRequest(jobId, jw.getValue()); r.setRevision(oldJw.getRevision()); getConnectionFactory().getConnection().update(getContext(), r); } else { // Add job name to list jgw.addJob(jobName); // Update job group UpdateRequest r = Requests.newUpdateRequest(getJobGroupsRepoId(jobGroup), jgw.getValue()); r.setRevision(jgw.getRevision()); getConnectionFactory().getConnection().update(getContext(), r); // Create job logger.debug("Creating Job {} in group {}", jobName, jobGroup); getConnectionFactory().getConnection().create(getContext(), getCreateRequest(jobId, jw.getValue().asMap())); } } catch (ResourceException e) { logger.warn("Error storing job", e); throw new JobPersistenceException("Error" + " storing job", e); } } } @Override public void storeJobAndTrigger(SchedulingContext context, JobDetail detail, Trigger trigger) throws ObjectAlreadyExistsException, JobPersistenceException { synchronized (lock) { storeJob(context, detail, false); storeTrigger(context, trigger, false); } } @Override public void storeTrigger(SchedulingContext context, Trigger trigger, boolean replaceExisting) throws ObjectAlreadyExistsException, JobPersistenceException { synchronized (lock) { String triggerName = trigger.getKey().getName(); String groupName = trigger.getKey().getGroup(); String triggerId = getTriggersRepoId(groupName, triggerName); TriggerGroupWrapper tgw = null; try { // Get trigger group tgw = getOrCreateTriggerGroupWrapper(groupName); } catch (ResourceException e) { logger.warn("Error storing trigger", e); throw new JobPersistenceException("Error" + " storing trigger", e); } List<String> triggerNames = tgw.getTriggerNames(); TriggerWrapper tw; try { tw = new TriggerWrapper(trigger, tgw.isPaused()); } catch (Exception e) { logger.warn("Error storing trigger", e); throw new JobPersistenceException("Error" + " storing trigger", e); } if (triggerNames.contains(triggerName) && !replaceExisting) { throw new ObjectAlreadyExistsException(trigger); } try { // Check if trigger name exists if (triggerNames.contains(triggerName)) { TriggerWrapper oldTw = getTriggerWrapper(groupName, triggerName); // Update trigger logger.debug("Updating Trigger {}", triggerId); UpdateRequest r = Requests.newUpdateRequest(triggerId, tw.getValue()); r.setRevision(oldTw.getRevision()); getConnectionFactory().getConnection().update(getContext(), r); } else { // Add trigger name to list tgw.addTrigger(triggerName); // Update trigger group UpdateRequest r = Requests.newUpdateRequest(getTriggerGroupsRepoId(groupName), tgw.getValue()); r.setRevision(tgw.getRevision()); getConnectionFactory().getConnection().update(getContext(), r); // Create trigger logger.debug("Creating Trigger {}", triggerId); getConnectionFactory().getConnection().create(getContext(), getCreateRequest(triggerId, tw.getValue().asMap())); } } catch (ResourceException e) { logger.warn("Error storing trigger", e); throw new JobPersistenceException("Error" + " storing trigger", e); } logger.debug("Adding waiting trigger {}", trigger.getName()); addWaitingTrigger(trigger); } } @Override public Trigger acquireNextTrigger(SchedulingContext context, long noLaterThan) throws JobPersistenceException { synchronized (lock) { logger.debug("Attempting to acquire the next trigger"); Trigger trigger = null; WaitingTriggers waitingTriggers = null; while (trigger == null && !shutdown) { try { waitingTriggers = getWaitingTriggers(); trigger = waitingTriggers.getTriggers().first(); } catch (NoSuchElementException e1) { logger.debug("No waiting triggers to acquire"); return null; } if (trigger == null) { logger.debug("No waiting triggers to acquire"); return null; } Date nextFireTime = trigger.getNextFireTime(); if (nextFireTime == null) { logger.debug("Trigger next fire time = null, removing"); removeWaitingTrigger(trigger); trigger = null; continue; } if (noLaterThan > 0) { if (nextFireTime.getTime() > noLaterThan) { logger.debug("Trigger fire time {} is later than {}, not acquiring", nextFireTime, new Date(noLaterThan)); return null; } } if(!removeWaitingTrigger(trigger)) { trigger = null; continue; } TriggerWrapper tw = getTriggerWrapper(trigger.getGroup(), trigger.getName()); if (hasTriggerMisfired(trigger)) { logger.debug("Attempting to process misfired trigger"); processTriggerMisfired(tw); if (trigger.getNextFireTime() != null) { addWaitingTrigger(trigger); } trigger = null; continue; } tw.setAcquired(true); trigger.setFireInstanceId(getFiredTriggerRecordId()); try { tw.updateTrigger(trigger); } catch (Exception e) { logger.warn("Error serializing trigger", e); addWaitingTrigger(trigger); throw new JobPersistenceException("Error serializing trigger", e); } updateTriggerInRepo(trigger.getGroup(), trigger.getName(), tw, tw.getRevision()); addAcquiredTrigger(trigger, instanceId); logger.debug("Acquired next trigger {} to be fired at {}", trigger.getName(), trigger.getNextFireTime()); return (Trigger)trigger.clone(); } logger.debug("No waiting triggers to acquire"); return null; } } @Override public void releaseAcquiredTrigger(SchedulingContext arg0, Trigger trigger) throws JobPersistenceException { synchronized (lock) { TriggerWrapper tw = getTriggerWrapper(trigger.getGroup(), trigger.getName()); if (tw == null) { logger.debug("Cannot release acquired trigger {} in group {}, trigger does not exist", trigger.getName(), trigger.getGroup()); return; } if (tw.isAcquired()) { tw.setAcquired(false); updateTriggerInRepo(trigger.getGroup(), trigger.getName(), tw, tw.getRevision()); addWaitingTrigger(trigger); removeAcquiredTrigger(trigger, instanceId); } else { logger.warn("Cannot release acquired trigger {} in group {}, trigger has not been acquired", trigger.getName(), trigger.getGroup()); } } } @Override public String[] getCalendarNames(SchedulingContext arg0) throws JobPersistenceException { List<String> names = null; try { names = getOrCreateRepoList(getCalendarNamesRepoId(), "names"); } catch (ResourceException e) { logger.warn("Error getting calendar names", e); throw new JobPersistenceException("Error getting calendar names", e); } if (names.size() > 0) { return names.toArray(new String[names.size()]); } else { return null; } } private List<TriggerWrapper> getTriggerWrappersForCalendar(String calName) throws JobPersistenceException { synchronized (lock) { ArrayList<TriggerWrapper> trigList = new ArrayList<TriggerWrapper>(); String[] groups = getTriggerGroupNames(null); for (String group : groups) { String[] names = getTriggerNames(null, group); for (String name : names) { TriggerWrapper tw = getTriggerWrapper(group, name); Trigger trigger = tw.getTrigger(); if (trigger.getCalendarName().equals(calName)) { trigList.add(tw); } } } return trigList; } } @Override public long getEstimatedTimeToReleaseAndAcquireTrigger() { return 5; } @Override public String[] getJobGroupNames(SchedulingContext context) throws JobPersistenceException { List<String> names = null; try { names = getOrCreateRepoList(getJobGroupNamesRepoId(), "names"); } catch (ResourceException e) { logger.warn("Error getting job group names", e); throw new JobPersistenceException("Error getting job group names", e); } return names.toArray(new String[names.size()]); } @Override public String[] getJobNames(SchedulingContext context, String groupName) throws JobPersistenceException { try { JsonValue fromRepo = readFromRepo(getJobGroupsRepoId(groupName)); if (fromRepo != null && !fromRepo.isNull()) { JobGroupWrapper jgw = new JobGroupWrapper(fromRepo); List<String> names = jgw.getJobNames(); return names.toArray(new String[names.size()]); } } catch (ResourceException e) { logger.warn("Error getting job names", e); throw new JobPersistenceException("Error getting job names", e); } return null; } @Override public int getNumberOfCalendars(SchedulingContext context) throws JobPersistenceException { String [] names = getCalendarNames(context); if (names != null) { return names.length; } return 0; } @Override public int getNumberOfJobs(SchedulingContext arg0) throws JobPersistenceException { String [] groupNames = getJobGroupNames(null); if (groupNames == null || groupNames.length == 0) { return 0; } int numOfJobs = 0; for (String groupName : groupNames) { String [] jobNames = getJobNames(null, groupName); if (jobNames != null) { numOfJobs += jobNames.length; } } return numOfJobs; } @Override public int getNumberOfTriggers(SchedulingContext arg0) throws JobPersistenceException { String [] groupNames = getTriggerGroupNames(null); if (groupNames == null || groupNames.length == 0) { return 0; } int numOfTriggers = 0; for (String groupName : groupNames) { String [] triggerNames = getTriggerNames(null, groupName); if (triggerNames != null) { numOfTriggers += triggerNames.length; } } return numOfTriggers; } @Override public Set getPausedTriggerGroups(SchedulingContext context) throws JobPersistenceException { List<String> names = null; try { Map<String, Object> pauseMap = getOrCreateRepo(getPausedTriggerGroupNamesRepoId()); names = (List<String>)pauseMap.get("paused"); if (names == null) { names = new ArrayList<String>(); } } catch (ResourceException e) { logger.warn("Error getting paused trigger groups", e); throw new JobPersistenceException("Error getting paused trigger groups", e); } return new HashSet<String>(names); } @Override public String[] getTriggerGroupNames(SchedulingContext context) throws JobPersistenceException { List<String> names = null; try { names = getOrCreateRepoList(getTriggerGroupNamesRepoId(), "names"); } catch (ResourceException e) { logger.warn("Error getting trigger group names", e); throw new JobPersistenceException("Error getting trigger group names", e); } return names.toArray(new String[names.size()]); } @Override public String[] getTriggerNames(SchedulingContext context, String groupName) throws JobPersistenceException { try { JsonValue fromRepo = readFromRepo(getTriggerGroupsRepoId(groupName)); if (fromRepo != null && !fromRepo.isNull()) { TriggerGroupWrapper tgw = new TriggerGroupWrapper(fromRepo); List<String> names = tgw.getTriggerNames(); return names.toArray(new String[names.size()]); } } catch (ResourceException e) { logger.warn("Error getting trigger names", e); throw new JobPersistenceException("Error getting trigger names", e); } return new String[0]; } @Override public int getTriggerState(SchedulingContext context, String triggerName, String triggerGroup) throws JobPersistenceException { String id = getTriggerId(triggerGroup, triggerName); int state = 0; logger.trace("Getting trigger state {}", id); JsonValue trigger = getTriggerFromRepo(triggerGroup, triggerName); if (trigger.isNull()) { return Trigger.STATE_NONE; } state = trigger.get("state").asInteger(); return state; } @Override public Trigger[] getTriggersForJob(SchedulingContext context, String jobName, String groupName) throws JobPersistenceException { synchronized (lock) { String[] triggerNames = getTriggerNames(context, groupName); List<Trigger> triggers = new ArrayList<Trigger>(); for (String name : triggerNames) { TriggerWrapper tw = getTriggerWrapper(groupName, name); Trigger trigger = tw.getTrigger(); if (trigger.getJobName().equals(jobName)) { triggers.add(trigger); } } logger.debug("Found {} triggers for group {}", triggers.size(), groupName); return triggers.toArray(new Trigger[triggers.size()]); } } @Override public boolean isClustered() { return true; } @Override public void pauseAll(SchedulingContext context) throws JobPersistenceException { String [] names = getTriggerGroupNames(context); for (String name : names) { pauseTriggerGroup(context, name); } } @Override public void pauseJob(SchedulingContext context, String jobName, String groupName) throws JobPersistenceException { synchronized (lock) { Trigger[] triggers = getTriggersForJob(context, jobName, groupName); for (Trigger trigger : triggers) { pauseTrigger(context, trigger.getName(), trigger.getGroup()); } } } @Override public void pauseJobGroup(SchedulingContext context, String groupName) throws JobPersistenceException { synchronized (lock) { try { // Get job group, set paused, and update JobGroupWrapper jgw = getOrCreateJobGroupWrapper(groupName); jgw.pause(); UpdateRequest r = Requests.newUpdateRequest(getJobGroupsRepoId(groupName), jgw.getValue()); r.setRevision(jgw.getRevision()); getConnectionFactory().getConnection().update(getContext(), r); // Get list of paused groups, add this group (if already paused, return), and update Map<String, Object> pauseMap = getOrCreateRepo(getPausedJobGroupNamesRepoId()); List<String> pausedGroups = (List<String>) pauseMap.get("paused"); if (pausedGroups == null) { pausedGroups = new ArrayList<String>(); pauseMap.put("paused", pausedGroups); } else if (pausedGroups.contains(groupName)) { return; } pausedGroups.add(groupName); String rev = (String)pauseMap.get("_rev"); r = Requests.newUpdateRequest(getPausedJobGroupNamesRepoId(), new JsonValue(pauseMap)); r.setRevision(rev); getConnectionFactory().getConnection().update(getContext(), r); List<String> jobNames = jgw.getJobNames(); for (String jobName : jobNames) { pauseJob(context, jobName, groupName); } } catch (JsonValueException e) { logger.warn("Error pausing job group {}", groupName, e); throw new JobPersistenceException("Error pausing job group", e); } catch (Exception e) { logger.warn("Error pausing job group {}", groupName, e); throw new JobPersistenceException("Error pausing job group", e); } } } @Override public void pauseTrigger(SchedulingContext context, String triggerName, String triggerGroup) throws JobPersistenceException { synchronized (lock) { TriggerWrapper tw = getTriggerWrapper(triggerGroup, triggerName); if (tw == null) { logger.warn("Cannot pause trigger {} in group {}, trigger does not exist", triggerName, triggerGroup); return; } Trigger trigger; try { trigger = tw.getTrigger(); } catch (Exception e) { logger.warn("Error deserializing trigger", e); throw new JobPersistenceException("Error deserializing trigger", e); } tw.pause(); // Update the trigger updateTriggerInRepo(triggerGroup, triggerName, tw, tw.getRevision()); // Remove trigger from waitingTriggers removeWaitingTrigger(trigger); } } @Override public void pauseTriggerGroup(SchedulingContext context, String groupName) throws JobPersistenceException { synchronized (lock) { try { // Get trigger group, set paused, and update TriggerGroupWrapper tgw = getOrCreateTriggerGroupWrapper(groupName); tgw.pause(); UpdateRequest r = Requests.newUpdateRequest(getTriggerGroupsRepoId(groupName), tgw.getValue()); r.setRevision(tgw.getRevision()); getConnectionFactory().getConnection().update(getContext(), r); // Get list of paused groups, add this group (if already paused, return), and update Map<String, Object> pauseMap = getOrCreateRepo(getPausedTriggerGroupNamesRepoId()); List<String> pausedGroups = (List<String>) pauseMap.get("paused"); if (pausedGroups == null) { pausedGroups = new ArrayList<String>(); pauseMap.put("paused", pausedGroups); } else if (pausedGroups.contains(groupName)) { return; } pausedGroups.add(groupName); String rev = (String)pauseMap.get("_rev"); r = Requests.newUpdateRequest(getPausedTriggerGroupNamesRepoId(), new JsonValue(pauseMap)); r.setRevision(rev); getConnectionFactory().getConnection().update(getContext(), r); List<String> triggerNames = tgw.getTriggerNames(); for (String triggerName : triggerNames) { pauseTrigger(context, triggerName, groupName); } } catch (JsonValueException e) { logger.warn("Error pausing trigger group {}", groupName, e); throw new JobPersistenceException("Error pausing trigger group", e); } catch (Exception e) { logger.warn("Error pausing trigger group {}", groupName, e); throw new JobPersistenceException("Error pausing trigger group", e); } } } @Override public boolean removeCalendar(SchedulingContext context, String name) throws JobPersistenceException { synchronized (lock) { List<TriggerWrapper> twList = getTriggerWrappersForCalendar(name); if (twList.size() > 0) { throw new JobPersistenceException("Calender cannot be removed if it is referenced by a trigger!"); } // Delete calendar logger.debug("Deleting calendar {}", name); try { CalendarWrapper cw = getCalendarWrapper(name); if (cw != null) { DeleteRequest r = Requests.newDeleteRequest(getCalendarsRepoId(name)); r.setRevision(cw.getRevision()); getConnectionFactory().getConnection().delete(getContext(), r); return true; } else { return false; } } catch (ResourceException e) { logger.warn("Error removing calendar {}", name, e); throw new JobPersistenceException("Error deleting calendar", e); } catch (Exception e) { logger.warn("Error removing calendar {}", name, e); throw new JobPersistenceException("Error deleting calendar", e); } } } @Override public boolean removeJob(SchedulingContext arg0, String jobName, String groupName) throws JobPersistenceException { synchronized (lock) { String jobId = getJobsRepoId(groupName, jobName); try { // Get job group JobGroupWrapper jgw = getOrCreateJobGroupWrapper(groupName); List<String> jobNames = jgw.getJobNames(); // Check if job name exists if (jobNames.contains(jobName)) { // Remove job from list jgw.removeJob(jobName); // Update job group UpdateRequest r = Requests.newUpdateRequest(getJobGroupsRepoId(groupName), jgw.getValue()); r.setRevision(jgw.getRevision()); getConnectionFactory().getConnection().update(getContext(), r); } // Delete job JobWrapper oldJw = getJobWrapper(groupName, jobName); if (oldJw == null) { return false; } logger.debug("Deleting job {} in group {}", jobName, groupName); DeleteRequest r = Requests.newDeleteRequest(jobId); r.setRevision(oldJw.getRevision()); getConnectionFactory().getConnection().delete(getContext(), r); return true; } catch (ResourceException e) { logger.warn("Error removing job {} ", jobName, e); throw new JobPersistenceException("Error removing job", e); } catch (Exception e) { logger.warn("Error removing job {} ", jobName, e); throw new JobPersistenceException("Error removing job", e); } } } @Override public boolean removeTrigger(SchedulingContext context, String triggerName, String groupName) throws JobPersistenceException { synchronized (lock) { String triggerId = getTriggersRepoId(groupName, triggerName); try { // Get trigger group TriggerGroupWrapper tgw = getOrCreateTriggerGroupWrapper(groupName); String rev = tgw.getRevision(); List<String> triggerNames = tgw.getTriggerNames(); // Check if trigger name exists if (triggerNames.contains(triggerName)) { // Remove trigger from list tgw.removeTrigger(triggerName); // Update trigger group UpdateRequest r = Requests.newUpdateRequest(getTriggerGroupsRepoId(groupName), tgw.getValue()); r.setRevision(tgw.getRevision()); getConnectionFactory().getConnection().update(getContext(), r); } // Attempt to remove from waiting triggers TriggerWrapper tw = getTriggerWrapper(groupName, triggerName); if (tw != null) { removeWaitingTrigger(tw.getTrigger()); removeAcquiredTrigger(tw.getTrigger(), instanceId); // Delete trigger rev = tw.getRevision(); logger.debug("Deleting trigger {} in group {}", triggerName, groupName); DeleteRequest r = Requests.newDeleteRequest(triggerId); r.setRevision(rev); getConnectionFactory().getConnection().delete(getContext(), r); String jobName = tw.getTrigger().getJobName(); JobWrapper jw = getJobWrapper(groupName, jobName); if (jw != null) { if (!jw.getJobDetail().isDurable()) { String jobId = getJobsRepoId(groupName, jobName); // Get job group JobGroupWrapper jgw = getOrCreateJobGroupWrapper(groupName); List<String> jobNames = jgw.getJobNames(); // Check if job name exists if (jobNames.contains(jobName)) { // Remove job from list jgw.removeJob(jobName); // Update job group UpdateRequest ru = Requests.newUpdateRequest(getJobGroupsRepoId(groupName), jgw.getValue()); ru.setRevision(jgw.getRevision()); getConnectionFactory().getConnection().update(getContext(), ru); } // Delete job logger.debug("Deleting job {} in group {}", jobName, groupName); r = Requests.newDeleteRequest(jobId); r.setRevision(jw.getRevision()); getConnectionFactory().getConnection().delete(getContext(), r); } } return true; } return false; } catch (Exception e) { logger.warn("Error removing trigger {} ", triggerName, e); throw new JobPersistenceException("Error removing trigger", e); } } } @Override public boolean replaceTrigger(SchedulingContext context, String triggerName, String groupName, Trigger newTrigger) throws JobPersistenceException { synchronized (lock) { boolean deleted = false; Trigger oldTrigger = null; TriggerWrapper tw = getTriggerWrapper(groupName, triggerName); if (tw != null) { oldTrigger = tw.getTrigger(); if (!oldTrigger.getJobName().equals(newTrigger.getJobName()) || !oldTrigger.getJobGroup().equals(newTrigger.getJobGroup())) { throw new JobPersistenceException("Error replacing trigger, new trigger references a different job"); } logger.debug("Replacing trigger {} in group {} with trigger {} in group {}", triggerName, groupName, newTrigger.getName(), groupName); deleted = removeTrigger(context, triggerName, groupName); } try { storeTrigger(context, newTrigger, false); } catch (JobPersistenceException e) { logger.warn("Error replacing trigger {}, restoring old trigger", triggerName, e); if (oldTrigger != null) { storeTrigger(context, oldTrigger, false); } throw e; } return deleted; } } @Override public void resumeAll(SchedulingContext context) throws JobPersistenceException { Set<String> names = getPausedTriggerGroups(context); for (String name : names) { resumeTriggerGroup(context, name); } } @Override public void resumeJob(SchedulingContext context, String jobName, String groupName) throws JobPersistenceException { synchronized (lock) { Trigger[] triggers = getTriggersForJob(context, jobName, groupName); for (Trigger trigger : triggers) { resumeTrigger(context, trigger.getName(), trigger.getGroup()); } } } @Override public void resumeJobGroup(SchedulingContext context, String groupName) throws JobPersistenceException { synchronized (lock) { try { // Get job group, resume, and update JobGroupWrapper jgw = getOrCreateJobGroupWrapper(groupName); jgw.resume(); String rev = jgw.getRevision(); UpdateRequest r = Requests.newUpdateRequest(getJobGroupsRepoId(groupName), jgw.getValue()); r.setRevision(rev); getConnectionFactory().getConnection().update(getContext(), r); // Get list of paused groups, remove this group (if not paused, return), and update Map<String, Object> pauseMap = getOrCreateRepo(getPausedJobGroupNamesRepoId()); List<String> pausedGroups = (List<String>) pauseMap.get("paused"); if (pausedGroups == null || !pausedGroups.contains(groupName)) { return; } pausedGroups.remove(groupName); rev = (String)pauseMap.get("_rev"); r = Requests.newUpdateRequest(getPausedJobGroupNamesRepoId(), new JsonValue(pauseMap)); r.setRevision(rev); getConnectionFactory().getConnection().update(getContext(), r); List<String> jobNames = jgw.getJobNames(); for (String jobName : jobNames) { resumeJob(context, jobName, groupName); } } catch (JsonValueException e) { logger.warn("Error resuming job group {}", groupName, e); throw new JobPersistenceException("Error resuming job group", e); } catch (Exception e) { logger.warn("Error resuming job group {}", groupName, e); throw new JobPersistenceException("Error resuming job group", e); } } } @Override public void resumeTrigger(SchedulingContext arg0, String triggerName, String triggerGroup) throws JobPersistenceException { synchronized (lock) { TriggerWrapper tw = getTriggerWrapper(triggerGroup, triggerName); if (tw == null) { logger.warn("Cannot resume trigger {} in group {}, trigger does not exist", triggerName, triggerGroup); return; } Trigger trigger; try { trigger = tw.getTrigger(); } catch (Exception e) { logger.warn("Error deserializing trigger", e); throw new JobPersistenceException("Error deserializing trigger", e); } tw.resume(); // Update the trigger updateTriggerInRepo(triggerGroup, triggerName, tw, tw.getRevision()); // Add trigger to waitingTriggers addWaitingTrigger(trigger); } } @Override public void resumeTriggerGroup(SchedulingContext context, String groupName) throws JobPersistenceException { synchronized (lock) { try { // Get trigger group, resume, and update TriggerGroupWrapper tgw = getOrCreateTriggerGroupWrapper(groupName); tgw.resume(); String rev = tgw.getRevision(); UpdateRequest r = Requests.newUpdateRequest(getTriggerGroupsRepoId(groupName), tgw.getValue()); r.setRevision(rev); getConnectionFactory().getConnection().update(getContext(), r); // Get list of paused groups, remove this group (if not present, return), and update Map<String, Object> pauseMap = getOrCreateRepo(getPausedTriggerGroupNamesRepoId()); List<String> pausedGroups = (List<String>) pauseMap.get("paused"); if (pausedGroups == null) { pausedGroups = new ArrayList<String>(); pauseMap.put("paused", pausedGroups); } else if (pausedGroups.contains(groupName)) { pausedGroups.remove(groupName); } rev = (String)pauseMap.get("_rev"); r = Requests.newUpdateRequest(getPausedTriggerGroupNamesRepoId(), new JsonValue(pauseMap)); r.setRevision(rev); getConnectionFactory().getConnection().update(getContext(), r); List<String> triggerNames = tgw.getTriggerNames(); for (String triggerName : triggerNames) { resumeTrigger(context, triggerName, groupName); } } catch (JsonValueException e) { logger.warn("Error pausing trigger group", groupName, e); throw new JobPersistenceException("Error deserializing trigger", e); } catch (Exception e) { logger.warn("Error pausing trigger group", groupName, e); throw new JobPersistenceException("Error pausing trigger group", e); } } } @Override public Calendar retrieveCalendar(SchedulingContext context, String name) throws JobPersistenceException { synchronized (lock) { if (name != null) { CalendarWrapper cw = getCalendarWrapper(name); if (cw != null) { try { return cw.getCalendar(); } catch (Exception e) { logger.warn("Error retrieving calendar", e); throw new JobPersistenceException("Error retrieving calendar", e); } } } return null; } } @Override public JobDetail retrieveJob(SchedulingContext context, String jobName, String jobGroup) throws JobPersistenceException { synchronized (lock) { if (logger.isTraceEnabled()) { logger.trace("Getting job {}", getJobsRepoId(jobGroup, jobName)); } JobWrapper jw = getJobWrapper(jobGroup, jobName); if (jw == null) { return null; } try { return jw.getJobDetail(); } catch (Exception e) { logger.warn("Error retrieving job", e); throw new JobPersistenceException("Error retrieving job", e); } } } public JobWrapper getJobWrapper(String jobGroup, String jobName) throws JobPersistenceException { try { if (logger.isTraceEnabled()) { logger.trace("Getting job {}", getJobsRepoId(jobGroup, jobName)); } Map<String, Object> jobMap = readFromRepo(getJobsRepoId(jobGroup, jobName)).asMap(); if (jobMap == null) { return null; } JobWrapper jw = new JobWrapper(jobMap); return jw; } catch (ResourceException e) { logger.warn("Error retrieving job", e); throw new JobPersistenceException("Error retrieving job", e); } catch (Exception e) { logger.warn("Error retrieving job", e); throw new JobPersistenceException("Error retrieving job", e); } } public CalendarWrapper getCalendarWrapper(String name) throws JobPersistenceException { synchronized (lock) { try { if (logger.isTraceEnabled()) { logger.trace("Getting calendar {}", getCalendarsRepoId(name)); } Map<String, Object> calMap = readFromRepo(getCalendarsRepoId(name)).asMap(); if (calMap == null) { return null; } CalendarWrapper cal = new CalendarWrapper(calMap); return cal; } catch (ResourceException e) { logger.warn("Error retrieving calendar", e); throw new JobPersistenceException("Error retrieving calendar", e); } catch (Exception e) { logger.warn("Error retrieving calendar", e); throw new JobPersistenceException("Error retrieving calendar", e); } } } @Override public Trigger retrieveTrigger(SchedulingContext context, String triggerName, String triggerGroup) throws JobPersistenceException { synchronized (lock) { try { TriggerWrapper tw = getTriggerWrapper(triggerGroup, triggerName); if (tw == null) { return null; } return tw.getTrigger(); } catch (Exception e) { logger.warn("Error retrieving trigger", e); throw new JobPersistenceException("Error retrieving trigger", e); } } } @Override public TriggerFiredBundle triggerFired(SchedulingContext context, Trigger trigger) throws JobPersistenceException { synchronized (lock) { logger.debug("Trigger {} has fired", trigger.getFullName()); TriggerWrapper tw; try { tw = getTriggerWrapper(trigger.getGroup(), trigger.getName()); } catch (Exception e) { logger.warn("Error setting trigger fired", e); throw new JobPersistenceException("Error setting trigger fired", e); } if (tw == null) { logger.warn("Error setting trigger fired, trigger does not exist"); return null; } if (!tw.isAcquired()) { logger.warn("Error setting trigger fired, trigger was not in acquired state"); } Trigger localTrigger; try { localTrigger = tw.getTrigger(); } catch (Exception e) { logger.warn("Error setting trigger fired", e); throw new JobPersistenceException("Error setting trigger fired", e); } Calendar triggerCalendar = null; if (localTrigger.getCalendarName() != null) { CalendarWrapper cw = getCalendarWrapper(localTrigger.getCalendarName()); if (cw == null) { logger.warn("Error setting trigger fired, cannot find trigger's calendar"); return null; } else { try { triggerCalendar = cw.getCalendar(); } catch (Exception e) { logger.warn("Error retrieving calendar", e); throw new JobPersistenceException("Error retrieving calendar", e); } } } Date previousFireTime = trigger.getPreviousFireTime(); removeWaitingTrigger(trigger); localTrigger.triggered(triggerCalendar); tw.updateTrigger(localTrigger); updateTriggerInRepo(localTrigger.getGroup(), localTrigger.getName(), tw, tw.getRevision()); trigger.triggered(triggerCalendar); // Set trigger into the normal/waiting state tw.setState(Trigger.STATE_NORMAL); TriggerFiredBundle tfb = new TriggerFiredBundle(retrieveJob(context, trigger.getJobName(), trigger.getJobGroup()), trigger, triggerCalendar, false, new Date(), trigger.getPreviousFireTime(), previousFireTime, trigger.getNextFireTime()); JobDetail job = tfb.getJobDetail(); if (job.isStateful()) { Trigger[] triggers = getTriggersForJob(context, job.getName(), job.getGroup()); for (Trigger t : triggers) { TriggerWrapper tmpTw = getTriggerWrapper(t.getGroup(), t.getName()); if (tmpTw != null) { if (tmpTw.getState() == Trigger.STATE_NORMAL || tmpTw.getState() == Trigger.STATE_PAUSED) { tmpTw.block(); } // update trigger in repo updateTriggerInRepo(t.getGroup(), tmpTw.getName(), tmpTw, tmpTw.getRevision()); removeWaitingTrigger(t); } } blockedJobs.add(getJobNameKey(job)); } else if (localTrigger.getNextFireTime() != null) { addWaitingTrigger(localTrigger); } return tfb; } } @Override public void triggeredJobComplete(SchedulingContext context, Trigger trigger, JobDetail jobDetail, int triggerInstCode) throws JobPersistenceException { synchronized(lock) { logger.debug("Job {} has completed", jobDetail.getFullName()); String jobKey = getJobNameKey(jobDetail); JobWrapper jw = getJobWrapper(jobDetail.getGroup(), jobDetail.getName()); JsonValue triggerValue = getTriggerFromRepo(trigger.getGroup(), trigger.getName()); TriggerWrapper tw = null; if (triggerValue != null && !triggerValue.isNull()) { tw = new TriggerWrapper(triggerValue.asMap()); } // Remove the acquired trigger (if acquired) removeAcquiredTrigger(trigger, instanceId); if (jw != null) { JobDetail jd; try { jd = jw.getJobDetail(); } catch (Exception e) { throw new JobPersistenceException("Error triggering job complete", e); } if (jd.isStateful()) { JobDataMap newData = jobDetail.getJobDataMap(); if (newData != null) { newData = (JobDataMap)newData.clone(); newData.clearDirtyFlag(); } jd.setJobDataMap(newData); blockedJobs.remove(getJobNameKey(jd)); Trigger[] triggers = getTriggersForJob(context, jd.getName(), jd.getGroup()); for (Trigger t : triggers) { TriggerWrapper tmpTw = getTriggerWrapper(t.getGroup(), t.getName()); if (tmpTw != null) { if (tmpTw.getState() == Trigger.STATE_BLOCKED) { tmpTw.unblock(); } // update trigger in repo updateTriggerInRepo(t.getGroup(), tmpTw.getName(), tmpTw, tmpTw.getRevision()); if (!tmpTw.isPaused()) { addWaitingTrigger(t); } } } schedulerSignaler.signalSchedulingChange(0L); } } else { blockedJobs.remove(jobKey); } if (tw != null) { if (triggerInstCode == Trigger.INSTRUCTION_DELETE_TRIGGER) { if (trigger.getNextFireTime() == null) { if (tw.getTrigger().getNextFireTime() == null) { removeTrigger(context, trigger.getName(), trigger.getGroup()); } } else { removeTrigger(context, trigger.getName(), trigger.getGroup()); schedulerSignaler.signalSchedulingChange(0L); } } else if (triggerInstCode == Trigger.INSTRUCTION_SET_TRIGGER_COMPLETE) { tw.setState(Trigger.STATE_COMPLETE); removeWaitingTrigger(tw.getTrigger()); schedulerSignaler.signalSchedulingChange(0L); } else if (triggerInstCode == Trigger.INSTRUCTION_SET_TRIGGER_ERROR) { logger.debug("Trigger {} set to ERROR state.", trigger.getFullName()); tw.setState(Trigger.STATE_ERROR); schedulerSignaler.signalSchedulingChange(0L); } else if (triggerInstCode == Trigger.INSTRUCTION_SET_ALL_JOB_TRIGGERS_ERROR) { logger.debug("All triggers of Job {} set to ERROR state.", trigger.getFullJobName()); setAllTriggersOfJobToState(trigger.getJobName(), trigger.getJobGroup(), Trigger.STATE_ERROR); schedulerSignaler.signalSchedulingChange(0L); } else if (triggerInstCode == Trigger.INSTRUCTION_SET_ALL_JOB_TRIGGERS_COMPLETE) { setAllTriggersOfJobToState(trigger.getJobName(), trigger.getJobGroup(), Trigger.STATE_COMPLETE); schedulerSignaler.signalSchedulingChange(0L); } } } } private CreateRequest getCreateRequest(String id, Map<String, Object> map) { String container = id.substring(0,id.lastIndexOf("/")); String newId = id.substring(id.lastIndexOf("/")+1); return Requests.newCreateRequest(container, newId, new JsonValue(map)); } private JsonValue readFromRepo(String repoId) throws ResourceException { try { return getConnectionFactory().getConnection().read(getContext(), Requests.newReadRequest(repoId)).getContent(); } catch (JobPersistenceException e) { return new JsonValue(null); } catch (NotFoundException e) { return new JsonValue(null); } } /** * Returns the misfire threshold for this JobStore * * @return the misfire threshold */ public long getMisfireThreshold() { return misfireThreshold; } /** * Sets the misfire threshold for this JobStore * * @param misfireThreshold the misfire threshold */ public void setMisfireThreshold(long misfireThreshold) { this.misfireThreshold = misfireThreshold; } /** * Sets all the Triggers of a Job to the same state. * * @param jobName the name of the Job * @param jobGroup the name of the Job Group * @param state the state * @throws JobPersistenceException */ protected void setAllTriggersOfJobToState(String jobName, String jobGroup, int state) throws JobPersistenceException { synchronized (lock) { Trigger[] triggers = getTriggersForJob(null, jobName, jobGroup); for (Trigger t : triggers) { TriggerWrapper tw = new TriggerWrapper(getTriggerFromRepo( t.getGroup(), t.getName()).asMap()); tw.setState(state); if (state != Trigger.STATE_NORMAL) { removeWaitingTrigger(tw.getTrigger()); } } } } /** * Gets a Trigger group from the repo and wraps it in a TriggerGroupWapper(). * Creates the group if it doesn't already exist * * @param groupName name of group * @return a trigger group wrapped in a TriggerGroupWrapper * @throws ObjectSetException */ private TriggerGroupWrapper getOrCreateTriggerGroupWrapper(String groupName) throws JobPersistenceException, ResourceException { synchronized (lock) { Map<String, Object> map; map = readFromRepo(getTriggerGroupsRepoId(groupName)).asMap(); TriggerGroupWrapper tgw = null; if (map == null) { // create if null tgw = new TriggerGroupWrapper(groupName); // create in repo JsonValue newValue = getConnectionFactory().getConnection().create(getContext(), getCreateRequest(getTriggerGroupsRepoId(groupName), tgw.getValue().asMap())).getContent(); tgw = new TriggerGroupWrapper(newValue); // Add to list of group names addTriggerGroupName(groupName); } else { // else build from map tgw = new TriggerGroupWrapper(map); } return tgw; } } /** * Gets a Job group from the repo and wraps it in a JobGroupWapper(). * Creates the group if it doesn't already exist * * @param groupName name of group * @return a job group wrapped in a JobGroupWrapper * @throws ObjectSetException */ private JobGroupWrapper getOrCreateJobGroupWrapper(String groupName) throws JobPersistenceException, ResourceException { synchronized (lock) { Map<String, Object> map; map = readFromRepo(getJobGroupsRepoId(groupName)).asMap(); JobGroupWrapper jgw = null; if (map == null) { // create if null jgw = new JobGroupWrapper(groupName); // create in repo JsonValue newValue = getConnectionFactory().getConnection().create(getContext(), getCreateRequest(getJobGroupsRepoId(groupName), jgw.getValue().asMap())).getContent(); jgw = new JobGroupWrapper(newValue); // Add to list of group names addJobGroupName(groupName); } else { // else build from map jgw = new JobGroupWrapper(map); } return jgw; } } /** * Adds a Trigger to the list of waiting triggers. * * @param trigger the Trigger to add * @throws JobPersistenceException * @throws ObjectSetException */ private void addWaitingTrigger(Trigger trigger) throws JobPersistenceException { synchronized (lock) { try { int retries = 0; while (writeRetries == -1 || retries <= writeRetries && !shutdown) { try { // update repo addRepoListName(getTriggerId(trigger.getGroup(), trigger.getName()), getWaitingTriggersRepoId(), "names"); break; } catch (PreconditionFailedException e) { logger.debug("Adding waiting trigger failed {}, retrying", e); retries++; } } } catch (ResourceException e) { throw new JobPersistenceException("Error adding waiting trigger", e); } } } /** * Removes a Trigger from the list of waiting triggers. * * @param trigger the Trigger to remove * @throws JobPersistenceException * @throws ObjectSetException */ private boolean removeWaitingTrigger(Trigger trigger) throws JobPersistenceException { synchronized (lock) { try { boolean result = false; int retries = 0; while (writeRetries == -1 || retries <= writeRetries && !shutdown) { try { result = removeRepoListName(getTriggerId(trigger.getGroup(), trigger.getName()), getWaitingTriggersRepoId(), "names"); break; } catch (PreconditionFailedException e) { logger.debug("Removing waiting trigger failed {}, retrying", e); retries++; } } return result; } catch (ResourceException e) { throw new JobPersistenceException("Error removing waiting trigger", e); } } } /** * Adds a Trigger to the list of acquired triggers. * * @param trigger the Trigger to add * @param instanceId the instance ID * @throws JobPersistenceException * @throws ObjectSetException */ private void addAcquiredTrigger(Trigger trigger, String instanceId) throws JobPersistenceException { synchronized (lock) { try { logger.debug("Adding acquired trigger {} for instance {}", trigger.getName(), instanceId); int retries = 0; while (writeRetries == -1 || retries <= writeRetries && !shutdown) { try { addRepoListName(getTriggerId(trigger.getGroup(), trigger.getName()), getAcquiredTriggersRepoId(), instanceId); break; } catch (PreconditionFailedException e) { logger.debug("Adding acquired trigger failed {}, retrying", e); retries++; } } } catch (ResourceException e) { throw new JobPersistenceException("Error adding waiting trigger", e); } } } /** * Removes a Trigger from the list of acquired triggers. * * @param trigger the Trigger to remove * @param instanceId the instance ID * @throws JobPersistenceException * @throws ObjectSetException */ private boolean removeAcquiredTrigger(Trigger trigger, String instanceId) throws JobPersistenceException { synchronized (lock) { try { logger.debug("Removing acquired trigger {} for instance {}", trigger.getName(), instanceId); boolean result = false; int retries = 0; while (writeRetries == -1 || retries <= writeRetries && !shutdown) { try { result = removeRepoListName(getTriggerId(trigger.getGroup(), trigger.getName()), getAcquiredTriggersRepoId(), instanceId); break; } catch (PreconditionFailedException e) { logger.debug("Removing acquired trigger failed {}, retrying", e); retries++; } } return result; } catch (ResourceException e) { throw new JobPersistenceException("Error removing waiting trigger", e); } } } /** * Returns the an AcquiredTriggers object which wraps the List of all triggers in the "acquired" state * * @param instanceId the ID of the instance that acquired the triggers * @return the WaitingTriggers object * @throws JobPersistenceException */ private AcquiredTriggers getAcquiredTriggers(String instanceId) throws JobPersistenceException { List<Trigger> acquiredTriggers = new ArrayList<Trigger>(); List<String> acquiredTriggerIds = new ArrayList<String>(); String repoId = getAcquiredTriggersRepoId(); String revision = null; Map<String, Object> map; try { try { map = getConnectionFactory().getConnection().read(getContext(), Requests.newReadRequest(repoId)).getContent().asMap(); } catch (NotFoundException e) { logger.trace("repo list {} not found, lets create it", "names"); map = null; } if (map == null) { map = new HashMap<String, Object>(); map.put(instanceId, acquiredTriggerIds); // create in repo map = getConnectionFactory().getConnection().create(getContext(), getCreateRequest(repoId, map)).getContent().asMap(); revision = (String)map.get("_rev"); } else { // else check if list exists in map acquiredTriggerIds = (List<String>) map.get(instanceId); revision = (String)map.get("_rev"); if (acquiredTriggerIds == null) { acquiredTriggerIds = new ArrayList<String>(); map.put(instanceId, acquiredTriggerIds); UpdateRequest r = Requests.newUpdateRequest(repoId, new JsonValue(map)); r.setRevision(revision); JsonValue updatedValue = getConnectionFactory().getConnection().update(getContext(), r).getContent();; revision = (String) updatedValue.asMap().get("_rev"); } } for (String id : acquiredTriggerIds) { TriggerWrapper tw = getTriggerWrapper(getGroupFromId(id), getNameFromId(id)); if (tw == null) { logger.warn("Could not add {} to list of waiting Triggers. Trigger not found in repo", id); } else { logger.trace("Found acquired trigger {} in group {}", tw.getName(),tw.getGroup()); acquiredTriggers.add(tw.getTrigger()); } } return new AcquiredTriggers(acquiredTriggers, revision); } catch (ResourceException e) { logger.warn("Error initializing waiting triggers", e); throw new JobPersistenceException("Error initializing waiting triggers", e); } } /** * Returns the a WaitingTriggers object which wraps the Tree of all triggers in the "waiting" state * * @return the WaitingTriggers object * @throws JobPersistenceException */ private WaitingTriggers getWaitingTriggers() throws JobPersistenceException { TreeSet<Trigger> waitingTriggers = new TreeSet(new TriggerComparator()); List<String> waitingTriggersRepoList = null; String repoId = getWaitingTriggersRepoId(); String revision = null; Map<String, Object> map; try { try { map = getConnectionFactory().getConnection().read(getContext(), Requests.newReadRequest(repoId)).getContent().asMap(); } catch (NotFoundException e) { logger.debug("repo list {} not found, lets create it", "names"); map = null; } if (map == null) { map = new HashMap<String, Object>(); waitingTriggersRepoList = new ArrayList<String>(); map.put("names", waitingTriggersRepoList); // create in repo map = getConnectionFactory().getConnection().create(getContext(), getCreateRequest(repoId, map)).getContent().asMap(); revision = (String)map.get("_rev"); } else { // else check if list exists in map waitingTriggersRepoList = (List<String>) map.get("names"); revision = (String)map.get("_rev"); if (waitingTriggersRepoList == null) { waitingTriggersRepoList = new ArrayList<String>(); map.put("names", waitingTriggersRepoList); UpdateRequest r = Requests.newUpdateRequest(repoId, new JsonValue(map)); r.setRevision(revision); JsonValue updatedValue = getConnectionFactory().getConnection().update(getContext(), r).getContent(); revision = (String) updatedValue.asMap().get("_rev"); } } for (String id : waitingTriggersRepoList) { TriggerWrapper tw = getTriggerWrapper(getGroupFromId(id), getNameFromId(id)); if (tw == null) { logger.warn("Could not add {} to list of waiting Triggers. Trigger not found in repo", id); } else { logger.debug("Found waiting trigger {} in group {}", tw.getName(),tw.getGroup()); waitingTriggers.add(tw.getTrigger()); } } return new WaitingTriggers(waitingTriggers, revision); } catch (ResourceException e) { logger.warn("Error initializing waiting triggers", e); throw new JobPersistenceException("Error initializing waiting triggers", e); } } /** * Adds a Trigger group name to the list of Trigger group names * * @param groupName the name to add * @throws JobPersistenceException * @throws ObjectSetException */ private void addTriggerGroupName(String groupName) throws JobPersistenceException, ResourceException { addRepoListName(groupName, getTriggerGroupNamesRepoId(), "names"); } /** * Adds a Job group name to the list of Job group names * * @param groupName the name to add * @throws JobPersistenceException * @throws ObjectSetException */ private void addJobGroupName(String groupName) throws JobPersistenceException, ResourceException { addRepoListName(groupName, getJobGroupNamesRepoId(), "names"); } /** * Adds a name to a list of names in the repo. * * @param name the name to add * @param id the repo id * @throws JobPersistenceException * @throws ObjectSetException */ private void addRepoListName(String name, String id, String list) throws JobPersistenceException, ResourceException { synchronized (lock) { logger.trace("Adding name: {} to {}", name, id); Map<String, Object> map = getOrCreateRepo(id); String rev = (String)map.get("_rev"); List<String> names = (List<String>) map.get(list); if (names == null) { names = new ArrayList<String>(); map.put(list, names); } if (!names.contains(name)) { names.add(name); } // update repo UpdateRequest r = Requests.newUpdateRequest(id, new JsonValue(map)); r.setRevision(rev); getConnectionFactory().getConnection().update(getContext(), r); } } /** * Removes a name from a list of names in the repo. * * @param name the name to remove * @param id the repo id * @return true if the name was removed, false otherwise (the name may not have been present) * @throws JobPersistenceException * @throws ObjectSetException */ private boolean removeRepoListName(String name, String id, String list) throws JobPersistenceException, ResourceException { synchronized (lock) { logger.trace("Removing name: {} from {}", name, id); Map<String, Object> map = getOrCreateRepo(id); String rev = (String)map.get("_rev"); List<String> names = (List<String>) map.get(list); if (names == null) { names = new ArrayList<String>(); map.put(list, names); } boolean result = names.remove(name); if (result) { // update repo UpdateRequest r = Requests.newUpdateRequest(id, new JsonValue(map)); r.setRevision(rev); getConnectionFactory().getConnection().update(getContext(), r); } return result; } } private Map<String, Object> getOrCreateRepo(String repoId) throws JobPersistenceException, ResourceException { synchronized (lock) { Map<String, Object> map; map = readFromRepo(repoId).asMap(); if (map == null) { map = new HashMap<String, Object>(); // create in repo logger.debug("Creating repo {}", repoId); map = getConnectionFactory().getConnection().create(getContext(), getCreateRequest(repoId, map)).getContent().asMap(); } return map; } } private List<String> getOrCreateRepoList(String repoId, String listId) throws JobPersistenceException, ResourceException { synchronized (lock) { List<String> list = null; Map<String, Object> map; String revision = null; try { map = getConnectionFactory().getConnection().read(getContext(), Requests.newReadRequest(repoId)).getContent().asMap(); } catch (NotFoundException e) { logger.debug("repo list {} not found, lets create it", listId); map = null; } if (map == null) { map = new HashMap<String, Object>(); list = new ArrayList<String>(); map.put(listId, list); // create in repo map = getConnectionFactory().getConnection().create(getContext(), getCreateRequest(repoId, map)).getContent().asMap(); } else { // else check if list exists in map list = (List<String>) map.get(listId); if (list == null) { list = new ArrayList<String>(); map.put(listId, list); revision = (String)map.get("_rev"); UpdateRequest r = Requests.newUpdateRequest(repoId, new JsonValue(map)); r.setRevision(revision); getConnectionFactory().getConnection().update(getContext(), r); } } return list; } } /** * Gets a trigger container from the repo as a JsonValue. * * @param name the name of the trigger * @param group the group id of the trigger * @return A JsonValue object containing the serialized trigger and other metadata * @throws JobPersistenceException */ private JsonValue getTriggerFromRepo(String group, String name) throws JobPersistenceException { synchronized (lock) { try { logger.trace("Getting trigger {} in group {} from repo", name, group); return readFromRepo(getTriggersRepoId(group, name)); } catch (ResourceException e) { logger.warn("Error getting trigger from repo", e); throw new JobPersistenceException("Error getting trigger from repo", e); } } } /** * Updates a trigger in the repo. * * @param name the name of the trigger * @param group the group id of the trigger * @param tw the TriggerWrapper representing the updated trigger * @throws JobPersistenceException */ private void updateTriggerInRepo(String group, String name, TriggerWrapper tw, String rev) throws JobPersistenceException { synchronized (lock) { try { if (logger.isTraceEnabled()) { logger.trace("Getting trigger {}", getTriggersRepoId(group, name)); } String repoId = getTriggersRepoId(group, name); UpdateRequest r = Requests.newUpdateRequest(repoId, tw.getValue()); r.setRevision(rev); getConnectionFactory().getConnection().update(getContext(), r); } catch (ResourceException e) { logger.warn("Error updating trigger in repo", e); throw new JobPersistenceException("Error updating trigger in repo", e); } } } /** * Gets a trigger as a TriggerWrapper object. * * @param group the group id of the trigger * @param name the name of the trigger * @return * @throws JobPersistenceException */ private TriggerWrapper getTriggerWrapper(String group, String name) throws JobPersistenceException { JsonValue triggerValue = getTriggerFromRepo(group, name); if (triggerValue.isNull()) { return null; } try { return new TriggerWrapper(triggerValue.asMap()); } catch (Exception e) { logger.warn("Error getting trigger", e); throw new JobPersistenceException("Error getting trigger", e); } } /** * Returns the record ID of a fired Trigger. * * @return the record ID */ private String getFiredTriggerRecordId() { return String.valueOf(ftrCtr.incrementAndGet()); } /** * Returns true if a Trigger has misfired, false otherwise. * * @param trigger the Trigger to check if misfired * @return true if a Trigger has misfired, false otherwise */ private boolean hasTriggerMisfired(Trigger trigger) { long now = System.currentTimeMillis(); Date nextFireTime = trigger.getNextFireTime(); if (nextFireTime.getTime() <= (now - misfireThreshold)) { return true; } return false; } /** * Processes a misfired Trigger. * * @param trigger the Trigger to process * @throws JobPersistenceException */ private void processTriggerMisfired(TriggerWrapper triggerWrapper) throws JobPersistenceException { Trigger trigger = triggerWrapper.getTrigger(); logger.trace("Signaling Trigger Listener Misfired"); schedulerSignaler.notifyTriggerListenersMisfired(trigger); Calendar calendar = retrieveCalendar(null, trigger.getCalendarName()); trigger.updateAfterMisfire(calendar); triggerWrapper.updateTrigger(trigger); updateTriggerInRepo(trigger.getGroup(), trigger.getName(), triggerWrapper, triggerWrapper.getRevision()); if (trigger.getNextFireTime() == null) { schedulerSignaler.notifySchedulerListenersFinalized(trigger); triggerWrapper.setState(Trigger.STATE_COMPLETE); // update trigger in repo updateTriggerInRepo(trigger.getGroup(), trigger.getName(), triggerWrapper, triggerWrapper.getRevision()); removeWaitingTrigger(trigger); } } /** * Returns a job name key used to uniquely identify a specific job. * * @param jobDetail * @return */ private String getJobNameKey(JobDetail jobDetail) { return new StringBuilder(jobDetail.getGroup()).append(UNIQUE_ID_SEPARATOR) .append(jobDetail.getName()).toString(); } /** * Cleans up any triggers previously acquired by this instance and processes any misfires. */ private void cleanUpInstance() { synchronized (lock) { try { logger.trace("Cleaning up instance"); // Get the list of all stored triggers List<Trigger> storedTriggers = new ArrayList<Trigger>(); String[] groupNames = getTriggerGroupNames(null); for (String groupName : groupNames) { String[] triggerNames = getTriggerNames(null, groupName); for (String triggerName : triggerNames) { storedTriggers.add(getTriggerWrapper(groupName, triggerName).getTrigger()); } } // Ignore triggers which are already present in the waiting list. WaitingTriggers wt = getWaitingTriggers(); TreeSet<Trigger> waitingTriggers = wt.getTriggers(); for (Trigger t : waitingTriggers) { storedTriggers.remove(t); } // Process and release any triggers which are acquired AcquiredTriggers at = getAcquiredTriggers(instanceId); List<Trigger> acquiredTriggers = at.getTriggers(); for (Trigger t : acquiredTriggers) { if (hasTriggerMisfired(t)) { logger.trace("Trigger {} has misfired", t.getName()); processTriggerMisfired(getTriggerWrapper(t.getGroup(), t.getName())); if (t.getNextFireTime() != null) { // Remove the trigger from the "acquired" triggers list removeAcquiredTrigger(t, instanceId); } } else { releaseAcquiredTrigger(null, t); } } // Add remaining triggers to the "waiting" triggers tree for (Trigger t : storedTriggers) { logger.trace("Adding trigger {} waitingTriggers", t.getName()); addWaitingTrigger(t); } } catch (JobPersistenceException e) { logger.warn("Error initializing RepoJobStore", e); } } } /** * A Comparator used to compare two Triggers */ protected class TriggerComparator implements Comparator { public int compare(Object t1, Object t2) { Trigger trigger1 = (Trigger)t1; Trigger trigger2 = (Trigger)t2; // First compare by nextFireTime() int result = ((Trigger)t1).compareTo((Trigger)t2); if (result == 0) { // If that didn't work, compare by priority result = trigger2.getPriority() - trigger1.getPriority(); if (result == 0) { // If that didn't work, compare by name and group result = trigger1.getFullName().compareTo(trigger2.getFullName()); } } return result; } } /** * A wrapper for the tree of waiting triggers */ protected class WaitingTriggers { private TreeSet<Trigger> triggers; private String revision; public WaitingTriggers(TreeSet<Trigger> triggers, String rev) { this.triggers = triggers; revision = rev; } public TreeSet<Trigger> getTriggers() { return triggers; } public String getRevision() { return revision; } public void setRevision(String rev) { revision = rev; } public List<String> getTriggerNamesList() { List<String> names = new ArrayList<String>(); Iterator<Trigger> iterator = triggers.iterator(); while (iterator.hasNext()) { Trigger t = iterator.next(); names.add(getTriggerId(t.getGroup(), t.getName())); } return names; } } /** * A wrapper for the list of acquired triggers */ protected class AcquiredTriggers { private List<Trigger> triggers; private String revision; public AcquiredTriggers(List<Trigger> triggers, String revision) { this.triggers = triggers; this.revision = revision; } public String getRevision() { return revision; } public List<Trigger> getTriggers() { return triggers; } public void setRevision(String rev) { revision = rev; } } public SchedulerSignaler getSchedulerSignaler() { return schedulerSignaler; } public void setSchedulerSignaler(SchedulerSignaler schedulerSignaler) { this.schedulerSignaler = schedulerSignaler; } @Override public boolean handleEvent(ClusterEvent event) { String eventInstanceId = event.getInstanceId(); switch (event.getType()) { case RECOVERY_INITIATED: try { // Free acquired triggers AcquiredTriggers triggers = getAcquiredTriggers(eventInstanceId); logger.debug("Found {} acquired triggers while recovering instance {}", triggers.getTriggers().size(), eventInstanceId); for (Trigger trigger : triggers.getTriggers()) { boolean removed = false; int retry = 0; // Remove the acquired trigger while (writeRetries == -1 || retry <= writeRetries && !shutdown) { try { removed = removeAcquiredTrigger(trigger, eventInstanceId); break; } catch (JobPersistenceException e) { logger.debug("Failed to remove acquired trigger", e); retry++; } } // Check if trigger was removed if (removed) { // Attempt to add to the trigger to the waiting trigger pool retry = 0; while (writeRetries == -1 || retry <= writeRetries && !shutdown) { try { addWaitingTrigger(trigger); break; } catch (JobPersistenceException e) { logger.debug("Failed to add waiting trigger", e); retry++; } } } logger.info("Recovered trigger {} from failed instance {}", trigger.getName(), eventInstanceId); // Update the recovery timestamp clusterManager.renewRecoveryLease(eventInstanceId); } // send notification schedulerSignaler.signalSchedulingChange(0L); } catch (JobPersistenceException e) { logger.warn("Error freeing acquired triggers of instance {}: {}", eventInstanceId, e.getMessage()); return false; } break; case INSTANCE_FAILED: break; case INSTANCE_RUNNING: break; } return true; } }