package net.joelinn.quartz.jobstore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.joelinn.quartz.jobstore.mixin.CronTriggerMixin;
import net.joelinn.quartz.jobstore.mixin.HolidayCalendarMixin;
import net.joelinn.quartz.jobstore.mixin.JobDetailMixin;
import net.joelinn.quartz.jobstore.mixin.TriggerMixin;
import org.quartz.Calendar;
import org.quartz.*;
import org.quartz.impl.calendar.HolidayCalendar;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.spi.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.util.Pool;
import java.util.*;
/**
* Joe Linn
* 7/12/2014
*/
@SuppressWarnings("unchecked")
public class RedisJobStore implements JobStore {
private static final Logger logger = LoggerFactory.getLogger(RedisJobStore.class);
private Pool<Jedis> jedisPool;
private JedisCluster jedisCluster;
/**
* Redis lock timeout in milliseconds
*/
protected int lockTimeout = 30_000;
/**
* Redis host
*/
protected String host;
/**
* Redis port
*/
protected int port = 6379;
/**
* Redis password
*/
protected String password;
/**
* Redis database
*/
protected short database = 0;
/**
* Redis sentinel master group name
*/
protected String masterGroupName;
/**
* Redis key prefix
*/
protected String keyPrefix = "";
/**
* Redis key delimiter
*/
protected String keyDelimiter = ":";
/**
* Set to true if a {@link JedisCluster} should be used. {@link #host} will be split on ',', and the resulting
* strings will be used as hostanmes for the cluster nodes.
*/
private boolean redisCluster;
/**
* Set to true if a {@link JedisSentinelPool} should be used. {@link #host} will be split on ',', and the
* resulting strings will be used as hostnames for the sentinel nodes. {@link #masterGroupName} will be
* used as the master group name.
*/
private boolean redisSentinel;
private int misfireThreshold = 60_000;
protected String instanceId;
protected AbstractRedisStorage storage;
/**
* socket connection timeout in ms
*/
protected int conTimeout = 3000;
/**
* connection retries counter
*/
protected int conRetries = 5;
/**
* socket timeout in ms
*/
protected int soTimeout = 3000;
public RedisJobStore setJedisPool(Pool<Jedis> jedisPool) {
this.jedisPool = jedisPool;
return this;
}
public RedisJobStore setJedisCluster(JedisCluster jedisCluster) {
this.jedisCluster = jedisCluster;
return this;
}
public void setMisfireThreshold(int misfireThreshold) {
this.misfireThreshold = misfireThreshold;
}
/**
* Called by the QuartzScheduler before the <code>JobStore</code> is
* used, in order to give the it a chance to initialize.
*
* @param loadHelper class loader helper
* @param signaler schedule signaler object
*/
@Override
public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler) throws SchedulerConfigException {
final RedisJobStoreSchema redisSchema = new RedisJobStoreSchema(keyPrefix, keyDelimiter);
ObjectMapper mapper = new ObjectMapper()
.addMixIn(CronTrigger.class, CronTriggerMixin.class)
.addMixIn(SimpleTrigger.class, TriggerMixin.class)
.addMixIn(JobDetail.class, JobDetailMixin.class)
.addMixIn(HolidayCalendar.class, HolidayCalendarMixin.class)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
if (redisCluster && jedisCluster == null) {
Set<HostAndPort> nodes = buildNodesSetFromHost();
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisCluster = new JedisCluster(nodes, this.conTimeout, this.soTimeout, this.conRetries, this.password,jedisPoolConfig);
storage = new RedisClusterStorage(redisSchema, mapper, signaler, instanceId, lockTimeout);
} else if (jedisPool == null) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setTestOnBorrow(true);
if (redisSentinel) {
Set<HostAndPort> nodes = buildNodesSetFromHost();
Set<String> nodesAsStrings = new HashSet<>();
for (HostAndPort node : nodes) {
nodesAsStrings.add(node.toString());
}
if (logger.isDebugEnabled()) {
logger.debug("Instantiating JedisSentinelPool using master " + masterGroupName + " and hosts " + host);
}
jedisPool = new JedisSentinelPool(masterGroupName, nodesAsStrings, jedisPoolConfig, Protocol.DEFAULT_TIMEOUT, password, database);
} else {
if (logger.isDebugEnabled()) {
logger.debug("Instantiating JedisPool using host " + host + " and port " + port);
}
jedisPool = new JedisPool(jedisPoolConfig, host, port, Protocol.DEFAULT_TIMEOUT, password, database);
}
storage = new RedisStorage(redisSchema, mapper, signaler, instanceId, lockTimeout);
}
storage.setMisfireThreshold(misfireThreshold);
}
/**
* Called by the QuartzScheduler to inform the <code>JobStore</code> that
* the scheduler has started.
*/
@Override
public void schedulerStarted() throws SchedulerException {
}
/**
* Called by the QuartzScheduler to inform the <code>JobStore</code> that
* the scheduler has been paused.
*/
@Override
public void schedulerPaused() {
// nothing to do
}
/**
* Called by the QuartzScheduler to inform the <code>JobStore</code> that
* the scheduler has resumed after being paused.
*/
@Override
public void schedulerResumed() {
// nothing to do
}
/**
* 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.
*/
@Override
public void shutdown() {
if(jedisPool != null){
jedisPool.destroy();
}
}
@Override
public boolean supportsPersistence() {
return true;
}
/**
* How long (in milliseconds) the <code>JobStore</code> implementation
* estimates that it will take to release a trigger and acquire a new one.
*/
@Override
public long getEstimatedTimeToReleaseAndAcquireTrigger() {
return 100;
}
/**
* Whether or not the <code>JobStore</code> implementation is redisCluster.
*/
@Override
public boolean isClustered() {
return true;
}
/**
* Store the given <code>{@link org.quartz.JobDetail}</code> and <code>{@link org.quartz.Trigger}</code>.
*
* @param newJob The <code>JobDetail</code> to be stored.
* @param newTrigger The <code>Trigger</code> to be stored.
* @throws org.quartz.ObjectAlreadyExistsException if a <code>Job</code> with the same name/group already
* exists.
*/
@Override
public void storeJobAndTrigger(final JobDetail newJob, final OperableTrigger newTrigger) throws ObjectAlreadyExistsException, JobPersistenceException {
try {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.storeJob(newJob, false, jedis);
storage.storeTrigger(newTrigger, false, jedis);
return null;
}
});
} catch (ObjectAlreadyExistsException e) {
logger.info("Job and / or trigger already exist in storage.", e);
throw e;
} catch (Exception e) {
logger.error("Could not store job.", e);
throw new JobPersistenceException(e.getMessage(), e);
}
}
/**
* Store the given <code>{@link org.quartz.JobDetail}</code>.
*
* @param newJob The <code>JobDetail</code> to be stored.
* @param replaceExisting If <code>true</code>, any <code>Job</code> existing in the
* <code>JobStore</code> with the same name & group should be
* over-written.
* @throws org.quartz.ObjectAlreadyExistsException if a <code>Job</code> with the same name/group already
* exists, and replaceExisting is set to false.
*/
@Override
public void storeJob(final JobDetail newJob, final boolean replaceExisting) throws ObjectAlreadyExistsException, JobPersistenceException {
try {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.storeJob(newJob, replaceExisting, jedis);
return null;
}
});
} catch (ObjectAlreadyExistsException e) {
logger.info("Job hash already exists");
throw e;
} catch (Exception e) {
logger.error("Could not store job.", e);
throw new JobPersistenceException(e.getMessage(), e);
}
}
@Override
public void storeJobsAndTriggers(final Map<JobDetail, Set<? extends Trigger>> triggersAndJobs, final boolean replace) throws ObjectAlreadyExistsException, JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
for (Map.Entry<JobDetail, Set<? extends Trigger>> entry : triggersAndJobs.entrySet()) {
storage.storeJob(entry.getKey(), replace, jedis);
for (Trigger trigger : entry.getValue()) {
storage.storeTrigger((OperableTrigger) trigger, replace, jedis);
}
}
return null;
}
}, "Could not store jobs and triggers.");
}
/**
* Remove (delete) the <code>{@link org.quartz.Job}</code> with the given
* key, and any <code>{@link org.quartz.Trigger}</code> s that reference
* it.
* <p/>
* <p>
* If removal of the <code>Job</code> results in an empty group, the
* group should be removed from the <code>JobStore</code>'s list of
* known group names.
* </p>
*
* @param jobKey the key of the job to be removed
* @return <code>true</code> if a <code>Job</code> with the given name &
* group was found and removed from the store.
*/
@Override
public boolean removeJob(final JobKey jobKey) throws JobPersistenceException {
return doWithLock(new LockCallback<Boolean>() {
@Override
public Boolean doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.removeJob(jobKey, jedis);
}
}, "Could not remove job.");
}
@Override
public boolean removeJobs(final List<JobKey> jobKeys) throws JobPersistenceException {
return doWithLock(new LockCallback<Boolean>() {
@Override
public Boolean doWithLock(JedisCommands jedis) throws JobPersistenceException {
boolean removed = jobKeys.size() > 0;
for (JobKey jobKey : jobKeys) {
removed = storage.removeJob(jobKey, jedis) && removed;
}
return removed;
}
}, "Could not remove jobs.");
}
/**
* Retrieve the <code>{@link org.quartz.JobDetail}</code> for the given
* <code>{@link org.quartz.Job}</code>.
*
* @param jobKey the {@link org.quartz.JobKey} describing the desired job
* @return The desired <code>Job</code>, or null if there is no match.
*/
@Override
public JobDetail retrieveJob(final JobKey jobKey) throws JobPersistenceException {
return doWithLock(new LockCallback<JobDetail>() {
@Override
public JobDetail doWithLock(JedisCommands jedis) throws JobPersistenceException {
try {
return storage.retrieveJob(jobKey, jedis);
} catch (ClassNotFoundException e) {
throw new JobPersistenceException("Error retrieving job: " + e.getMessage(), e);
}
}
}, "Could not retrieve job.");
}
/**
* Store the given <code>{@link org.quartz.Trigger}</code>.
*
* @param newTrigger The <code>Trigger</code> to be stored.
* @param replaceExisting If <code>true</code>, any <code>Trigger</code> existing in
* the <code>JobStore</code> with the same name & group should
* be over-written.
* @throws org.quartz.ObjectAlreadyExistsException if a <code>Trigger</code> with the same name/group already
* exists, and replaceExisting is set to false.
* @see #pauseTriggers(org.quartz.impl.matchers.GroupMatcher)
*/
@Override
public void storeTrigger(final OperableTrigger newTrigger, final boolean replaceExisting) throws ObjectAlreadyExistsException, JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.storeTrigger(newTrigger, replaceExisting, jedis);
return null;
}
}, "Could not store trigger.");
}
/**
* Remove (delete) the <code>{@link org.quartz.Trigger}</code> with the
* given key.
* <p/>
* <p>
* If removal of the <code>Trigger</code> results in an empty group, the
* group should be removed from the <code>JobStore</code>'s list of
* known group names.
* </p>
* <p/>
* <p>
* If removal of the <code>Trigger</code> results in an 'orphaned' <code>Job</code>
* that is not 'durable', then the <code>Job</code> should be deleted
* also.
* </p>
*
* @param triggerKey the key of the trigger to be removed
* @return <code>true</code> if a <code>Trigger</code> with the given
* name & group was found and removed from the store.
*/
@Override
public boolean removeTrigger(final TriggerKey triggerKey) throws JobPersistenceException {
return doWithLock(new LockCallback<Boolean>() {
@Override
public Boolean doWithLock(JedisCommands jedis) throws JobPersistenceException {
try {
return storage.removeTrigger(triggerKey, jedis);
} catch (ClassNotFoundException e) {
throw new JobPersistenceException("Error removing trigger: " + e.getMessage(), e);
}
}
}, "Could not remove trigger.");
}
@Override
public boolean removeTriggers(final List<TriggerKey> triggerKeys) throws JobPersistenceException {
return doWithLock(new LockCallback<Boolean>() {
@Override
public Boolean doWithLock(JedisCommands jedis) throws JobPersistenceException {
boolean removed = triggerKeys.size() > 0;
for (TriggerKey triggerKey : triggerKeys) {
try {
removed = storage.removeTrigger(triggerKey, jedis) && removed;
} catch (ClassNotFoundException e) {
throw new JobPersistenceException(e.getMessage(), e);
}
}
return removed;
}
}, "Could not remove trigger.");
}
/**
* Remove (delete) the <code>{@link org.quartz.Trigger}</code> with the
* given key, and store the new given one - which must be associated
* with the same job.
*
* @param triggerKey the key of the trigger to be replaced
* @param newTrigger The new <code>Trigger</code> to be stored.
* @return <code>true</code> if a <code>Trigger</code> with the given
* name & group was found and removed from the store.
*/
@Override
public boolean replaceTrigger(final TriggerKey triggerKey, final OperableTrigger newTrigger) throws JobPersistenceException {
return doWithLock(new LockCallback<Boolean>() {
@Override
public Boolean doWithLock(JedisCommands jedis) throws JobPersistenceException {
try {
return storage.replaceTrigger(triggerKey, newTrigger, jedis);
} catch (ClassNotFoundException e) {
throw new JobPersistenceException(e.getMessage(), e);
}
}
}, "Could not remove trigger.");
}
/**
* Retrieve the given <code>{@link org.quartz.Trigger}</code>.
*
* @param triggerKey the key of the desired trigger
* @return The desired <code>Trigger</code>, or null if there is no
* match.
*/
@Override
public OperableTrigger retrieveTrigger(final TriggerKey triggerKey) throws JobPersistenceException {
return doWithLock(new LockCallback<OperableTrigger>() {
@Override
public OperableTrigger doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.retrieveTrigger(triggerKey, jedis);
}
}, "Could not retrieve trigger.");
}
/**
* Determine whether a {@link org.quartz.Job} with the given identifier already
* exists within the scheduler.
*
* @param jobKey the identifier to check for
* @return true if a Job exists with the given identifier
* @throws org.quartz.JobPersistenceException
*/
@Override
public boolean checkExists(final JobKey jobKey) throws JobPersistenceException {
return doWithLock(new LockCallback<Boolean>() {
@Override
public Boolean doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.checkExists(jobKey, jedis);
}
}, "Could not check if job exists: " + jobKey);
}
/**
* Determine whether a {@link org.quartz.Trigger} with the given identifier already
* exists within the scheduler.
*
* @param triggerKey the identifier to check for
* @return true if a Trigger exists with the given identifier
* @throws org.quartz.JobPersistenceException
*/
@Override
public boolean checkExists(final TriggerKey triggerKey) throws JobPersistenceException {
return doWithLock(new LockCallback<Boolean>() {
@Override
public Boolean doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.checkExists(triggerKey, jedis);
}
}, "Could not check if trigger exists: " + triggerKey);
}
/**
* Clear (delete!) all scheduling data - all {@link org.quartz.Job}s, {@link org.quartz.Trigger}s
* {@link org.quartz.Calendar}s.
*
* @throws org.quartz.JobPersistenceException
*/
@Override
public void clearAllSchedulingData() throws JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
try {
storage.clearAllSchedulingData(jedis);
} catch (ClassNotFoundException e) {
throw new JobPersistenceException("Could not clear scheduling data.");
}
return null;
}
}, "Could not clear scheduling data.");
}
/**
* Store the given <code>{@link org.quartz.Calendar}</code>.
*
* @param name The name of the calendar
* @param calendar The <code>Calendar</code> to be stored.
* @param replaceExisting If <code>true</code>, any <code>Calendar</code> existing
* in the <code>JobStore</code> with the same name & group
* should be over-written.
* @param updateTriggers If <code>true</code>, any <code>Trigger</code>s existing
* in the <code>JobStore</code> that reference an existing
* Calendar with the same name with have their next fire time
* re-computed with the new <code>Calendar</code>.
* @throws org.quartz.ObjectAlreadyExistsException if a <code>Calendar</code> with the same name already
* exists, and replaceExisting is set to false.
*/
@Override
public void storeCalendar(final String name, final Calendar calendar, final boolean replaceExisting, final boolean updateTriggers) throws ObjectAlreadyExistsException, JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.storeCalendar(name, calendar, replaceExisting, updateTriggers, jedis);
return null;
}
}, "Could not store calendar.");
}
/**
* Remove (delete) the <code>{@link org.quartz.Calendar}</code> with the
* given name.
* <p/>
* <p>
* If removal of the <code>Calendar</code> would result in
* <code>Trigger</code>s pointing to non-existent calendars, then a
* <code>JobPersistenceException</code> will be thrown.</p>
* *
*
* @param calName The name of the <code>Calendar</code> to be removed.
* @return <code>true</code> if a <code>Calendar</code> with the given name
* was found and removed from the store.
*/
@Override
public boolean removeCalendar(final String calName) throws JobPersistenceException {
return doWithLock(new LockCallback<Boolean>() {
@Override
public Boolean doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.removeCalendar(calName, jedis);
}
}, "Could not remove calendar.");
}
/**
* Retrieve the given <code>{@link org.quartz.Trigger}</code>.
*
* @param calName The name of the <code>Calendar</code> to be retrieved.
* @return The desired <code>Calendar</code>, or null if there is no
* match.
*/
@Override
public Calendar retrieveCalendar(final String calName) throws JobPersistenceException {
return doWithLock(new LockCallback<Calendar>() {
@Override
public Calendar doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.retrieveCalendar(calName, jedis);
}
}, "Could not retrieve calendar.");
}
/**
* Get the number of <code>{@link org.quartz.Job}</code> s that are
* stored in the <code>JobsStore</code>.
*/
@Override
public int getNumberOfJobs() throws JobPersistenceException {
return doWithLock(new LockCallback<Integer>() {
@Override
public Integer doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getNumberOfJobs(jedis);
}
}, "Could not get number of jobs.");
}
/**
* Get the number of <code>{@link org.quartz.Trigger}</code> s that are
* stored in the <code>JobsStore</code>.
*/
@Override
public int getNumberOfTriggers() throws JobPersistenceException {
return doWithLock(new LockCallback<Integer>() {
@Override
public Integer doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getNumberOfTriggers(jedis);
}
}, "Could not get number of jobs.");
}
/**
* Get the number of <code>{@link org.quartz.Calendar}</code> s that are
* stored in the <code>JobsStore</code>.
*/
@Override
public int getNumberOfCalendars() throws JobPersistenceException {
return doWithLock(new LockCallback<Integer>() {
@Override
public Integer doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getNumberOfCalendars(jedis);
}
}, "Could not get number of jobs.");
}
/**
* Get the keys of all of the <code>{@link org.quartz.Job}</code> s that
* have the given group name.
* <p/>
* <p>
* If there are no jobs in the given group name, the result should be
* an empty collection (not <code>null</code>).
* </p>
*
* @param matcher the matcher for job key comparison
*/
@Override
public Set<JobKey> getJobKeys(final GroupMatcher<JobKey> matcher) throws JobPersistenceException {
return doWithLock(new LockCallback<Set<JobKey>>() {
@Override
public Set<JobKey> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getJobKeys(matcher, jedis);
}
}, "Could not retrieve JobKeys.");
}
/**
* Get the names of all of the <code>{@link org.quartz.Trigger}</code> s
* that have the given group name.
* <p/>
* <p>
* If there are no triggers in the given group name, the result should be a
* zero-length array (not <code>null</code>).
* </p>
*
* @param matcher the matcher with which to compare trigger groups
*/
@Override
public Set<TriggerKey> getTriggerKeys(final GroupMatcher<TriggerKey> matcher) throws JobPersistenceException {
return doWithLock(new LockCallback<Set<TriggerKey>>() {
@Override
public Set<TriggerKey> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getTriggerKeys(matcher, jedis);
}
}, "Could not retrieve TriggerKeys.");
}
/**
* Get the names of all of the <code>{@link org.quartz.Job}</code>
* groups.
* <p/>
* <p>
* If there are no known group names, the result should be a zero-length
* array (not <code>null</code>).
* </p>
*/
@Override
public List<String> getJobGroupNames() throws JobPersistenceException {
return doWithLock(new LockCallback<List<String>>() {
@Override
public List<String> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getJobGroupNames(jedis);
}
}, "Could not retrieve job group names.");
}
/**
* Get the names of all of the <code>{@link org.quartz.Trigger}</code>
* groups.
* <p/>
* <p>
* If there are no known group names, the result should be a zero-length
* array (not <code>null</code>).
* </p>
*/
@Override
public List<String> getTriggerGroupNames() throws JobPersistenceException {
return doWithLock(new LockCallback<List<String>>() {
@Override
public List<String> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getTriggerGroupNames(jedis);
}
}, "Could not retrieve trigger group names.");
}
/**
* Get the names of all of the <code>{@link org.quartz.Calendar}</code> s
* in the <code>JobStore</code>.
* <p/>
* <p>
* If there are no Calendars in the given group name, the result should be
* a zero-length array (not <code>null</code>).
* </p>
*/
@Override
public List<String> getCalendarNames() throws JobPersistenceException {
return doWithLock(new LockCallback<List<String>>() {
@Override
public List<String> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getCalendarNames(jedis);
}
}, "Could not retrieve calendar names.");
}
/**
* Get all of the Triggers that are associated to the given Job.
* <p/>
* <p>
* If there are no matches, a zero-length array should be returned.
* </p>
*
* @param jobKey the key of the job for which to retrieve triggers
*/
@Override
public List<OperableTrigger> getTriggersForJob(final JobKey jobKey) throws JobPersistenceException {
return doWithLock(new LockCallback<List<OperableTrigger>>() {
@Override
public List<OperableTrigger> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getTriggersForJob(jobKey, jedis);
}
}, "Could not retrieve triggers for job.");
}
/**
* Get the current state of the identified <code>{@link org.quartz.Trigger}</code>.
*
* @param triggerKey the key of the trigger for which to retrieve state
* @see org.quartz.Trigger.TriggerState
*/
@Override
public Trigger.TriggerState getTriggerState(final TriggerKey triggerKey) throws JobPersistenceException {
return doWithLock(new LockCallback<Trigger.TriggerState>() {
@Override
public Trigger.TriggerState doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getTriggerState(triggerKey, jedis);
}
}, "Could not retrieve trigger state.");
}
/**
* Pause the <code>{@link org.quartz.Trigger}</code> with the given key.
*
* @param triggerKey the key for the trigger to be paused
* @see #resumeTrigger(org.quartz.TriggerKey)
*/
@Override
public void pauseTrigger(final TriggerKey triggerKey) throws JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.pauseTrigger(triggerKey, jedis);
return null;
}
}, "Could not pause trigger.");
}
/**
* Pause all of the <code>{@link org.quartz.Trigger}s</code> in the
* given group.
* <p/>
* <p/>
* <p>
* The JobStore should "remember" that the group is paused, and impose the
* pause on any new triggers that are added to the group while the group is
* paused.
* </p>
*
* @param matcher a trigger group matcher
*/
@Override
public Collection<String> pauseTriggers(final GroupMatcher<TriggerKey> matcher) throws JobPersistenceException {
return doWithLock(new LockCallback<Collection<String>>() {
@Override
public Collection<String> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.pauseTriggers(matcher, jedis);
}
}, "Could not pause triggers.");
}
/**
* Pause the <code>{@link org.quartz.Job}</code> with the given name - by
* pausing all of its current <code>Trigger</code>s.
*
* @param jobKey the key of the job to be paused
* @see #resumeJob(org.quartz.JobKey)
*/
@Override
public void pauseJob(final JobKey jobKey) throws JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.pauseJob(jobKey, jedis);
return null;
}
}, "Could not pause job.");
}
/**
* Pause all of the <code>{@link org.quartz.Job}s</code> in the given
* group - by pausing all of their <code>Trigger</code>s.
* <p/>
* <p>
* The JobStore should "remember" that the group is paused, and impose the
* pause on any new jobs that are added to the group while the group is
* paused.
* </p>
*
* @param groupMatcher the mather which will determine which job group should be paused
* @see #resumeJobs(org.quartz.impl.matchers.GroupMatcher)
*/
@Override
public Collection<String> pauseJobs(final GroupMatcher<JobKey> groupMatcher) throws JobPersistenceException {
return doWithLock(new LockCallback<Collection<String>>() {
@Override
public Collection<String> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.pauseJobs(groupMatcher, jedis);
}
}, "Could not pause jobs.");
}
/**
* Resume (un-pause) the <code>{@link org.quartz.Trigger}</code> with the
* given key.
* <p/>
* <p>
* If the <code>Trigger</code> missed one or more fire-times, then the
* <code>Trigger</code>'s misfire instruction will be applied.
* </p>
*
* @param triggerKey the key of the trigger to be resumed
* @see #pauseTrigger(org.quartz.TriggerKey)
*/
@Override
public void resumeTrigger(final TriggerKey triggerKey) throws JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.resumeTrigger(triggerKey, jedis);
return null;
}
}, "Could not resume trigger.");
}
/**
* Resume (un-pause) all of the <code>{@link org.quartz.Trigger}s</code>
* in the given group.
* <p/>
* <p>
* If any <code>Trigger</code> missed one or more fire-times, then the
* <code>Trigger</code>'s misfire instruction will be applied.
* </p>
*
* @param matcher a trigger group matcher
* @see #pauseTriggers(org.quartz.impl.matchers.GroupMatcher)
*/
@Override
public Collection<String> resumeTriggers(final GroupMatcher<TriggerKey> matcher) throws JobPersistenceException {
return doWithLock(new LockCallback<Collection<String>>() {
@Override
public Collection<String> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.resumeTriggers(matcher, jedis);
}
}, "Could not resume trigger group(s).");
}
@Override
public Set<String> getPausedTriggerGroups() throws JobPersistenceException {
return doWithLock(new LockCallback<Set<String>>() {
@Override
public Set<String> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.getPausedTriggerGroups(jedis);
}
}, "Could not retrieve paused trigger groups.");
}
/**
* Resume (un-pause) the <code>{@link org.quartz.Job}</code> with the
* given key.
* <p/>
* <p>
* If any of the <code>Job</code>'s<code>Trigger</code> s missed one
* or more fire-times, then the <code>Trigger</code>'s misfire
* instruction will be applied.
* </p>
*
* @param jobKey the key of the job to be resumed
* @see #pauseJob(org.quartz.JobKey)
*/
@Override
public void resumeJob(final JobKey jobKey) throws JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.resumeJob(jobKey, jedis);
return null;
}
}, "Could not resume job.");
}
/**
* Resume (un-pause) all of the <code>{@link org.quartz.Job}s</code> in
* the given group.
* <p/>
* <p>
* If any of the <code>Job</code> s had <code>Trigger</code> s that
* missed one or more fire-times, then the <code>Trigger</code>'s
* misfire instruction will be applied.
* </p>
*
* @param matcher the matcher for job group name comparison
* @see #pauseJobs(org.quartz.impl.matchers.GroupMatcher)
*/
@Override
public Collection<String> resumeJobs(final GroupMatcher<JobKey> matcher) throws JobPersistenceException {
return doWithLock(new LockCallback<Collection<String>>() {
@Override
public Collection<String> doWithLock(JedisCommands jedis) throws JobPersistenceException {
return storage.resumeJobs(matcher, jedis);
}
}, "Could not resume jobs.");
}
/**
* Pause all triggers - equivalent of calling <code>pauseTriggerGroup(group)</code>
* on every group.
* <p/>
* <p>
* When <code>resumeAll()</code> is called (to un-pause), trigger misfire
* instructions WILL be applied.
* </p>
*
* @see #resumeAll()
* @see #pauseTriggers(org.quartz.impl.matchers.GroupMatcher)
*/
@Override
public void pauseAll() throws JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.pauseAll(jedis);
return null;
}
}, "Could not pause all triggers.");
}
/**
* Resume (un-pause) all triggers - equivalent of calling <code>resumeTriggerGroup(group)</code>
* on every group.
* <p/>
* <p>
* If any <code>Trigger</code> missed one or more fire-times, then the
* <code>Trigger</code>'s misfire instruction will be applied.
* </p>
*
* @see #pauseAll()
*/
@Override
public void resumeAll() throws JobPersistenceException {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.resumeAll(jedis);
return null;
}
}, "Could not resume all triggers.");
}
/**
* Get a handle to the next trigger to be fired, and mark it as 'reserved'
* by the calling scheduler.
*
* @param noLaterThan If > 0, the JobStore should only return a Trigger
* that will fire no later than the time represented in this value as
* milliseconds.
* @param maxCount the maximum number of triggers to return
* @param timeWindow @see #releaseAcquiredTrigger(Trigger)
*/
@Override
public List<OperableTrigger> acquireNextTriggers(final long noLaterThan, final int maxCount, final long timeWindow) throws JobPersistenceException {
return doWithLock(new LockCallback<List<OperableTrigger>>() {
@Override
public List<OperableTrigger> doWithLock(JedisCommands jedis) throws JobPersistenceException {
try {
return storage.acquireNextTriggers(noLaterThan, maxCount, timeWindow, jedis);
} catch (ClassNotFoundException e) {
throw new JobPersistenceException(e.getMessage(), e);
}
}
}, "Could not acquire next triggers.");
}
/**
* Inform the <code>JobStore</code> that the scheduler no longer plans to
* fire the given <code>Trigger</code>, that it had previously acquired
* (reserved).
*
* @param trigger the trigger to be released
*/
@Override
public void releaseAcquiredTrigger(final OperableTrigger trigger) {
try {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
storage.releaseAcquiredTrigger(trigger, jedis);
return null;
}
}, "Could not release acquired trigger.");
} catch (JobPersistenceException e) {
logger.error("Could not release acquired trigger.", e);
}
}
/**
* Inform the <code>JobStore</code> that the scheduler is now firing the
* given <code>Trigger</code> (executing its associated <code>Job</code>),
* that it had previously acquired (reserved).
*
* @param triggers a list of triggers which are being fired by the scheduler
* @return may return null if all the triggers or their calendars no longer exist, or
* if the trigger was not successfully put into the 'executing'
* state. Preference is to return an empty list if none of the triggers
* could be fired.
*/
@Override
public List<TriggerFiredResult> triggersFired(final List<OperableTrigger> triggers) throws JobPersistenceException {
return doWithLock(new LockCallback<List<TriggerFiredResult>>() {
@Override
public List<TriggerFiredResult> doWithLock(JedisCommands jedis) throws JobPersistenceException {
try {
return storage.triggersFired(triggers, jedis);
} catch (ClassNotFoundException e) {
throw new JobPersistenceException(e.getMessage(), e);
}
}
}, "Could not set triggers as fired.");
}
/**
* Inform the <code>JobStore</code> that the scheduler has completed the
* firing of the given <code>Trigger</code> (and the execution of its
* associated <code>Job</code> completed, threw an exception, or was vetoed),
* and that the <code>{@link org.quartz.JobDataMap}</code>
* in the given <code>JobDetail</code> should be updated if the <code>Job</code>
* is stateful.
*
* @param trigger the completetd trigger
* @param jobDetail the completed job
* @param triggerInstCode the trigger completion code
*/
@Override
public void triggeredJobComplete(final OperableTrigger trigger, final JobDetail jobDetail, final Trigger.CompletedExecutionInstruction triggerInstCode) {
try {
doWithLock(new LockCallbackWithoutResult() {
@Override
public Void doWithLock(JedisCommands jedis) throws JobPersistenceException {
try {
storage.triggeredJobComplete(trigger, jobDetail, triggerInstCode, jedis);
} catch (ClassNotFoundException e) {
logger.error("Could not handle job completion.", e);
}
return null;
}
});
} catch (JobPersistenceException e) {
logger.error("Could not handle job completion.", e);
}
}
/**
* Perform Redis operations while possessing lock
* @param callback operation(s) to be performed during lock
* @param <T> return type
* @return response from callback, if any
* @throws JobPersistenceException
*/
private <T> T doWithLock(LockCallback<T> callback) throws JobPersistenceException {
return doWithLock(callback, null);
}
/**
* Perform a redis operation while lock is acquired
* @param callback a callback containing the actions to perform during lock
* @param errorMessage optional error message to include in exception should an error arise
* @param <T> return class
* @return the result of the actions performed while locked, if any
* @throws JobPersistenceException
*/
private <T> T doWithLock(LockCallback<T> callback, String errorMessage) throws JobPersistenceException {
JedisCommands jedis = null;
try {
jedis = getResource();
try {
storage.waitForLock(jedis);
return callback.doWithLock(jedis);
} catch (ObjectAlreadyExistsException e) {
throw e;
} catch (Exception e) {
if (errorMessage == null || errorMessage.isEmpty()) {
errorMessage = "Job storage error.";
}
throw new JobPersistenceException(errorMessage, e);
} finally {
storage.unlock(jedis);
}
} finally {
if (jedis != null && jedis instanceof Jedis) {
// only close if we're not using a JedisCluster instance
((Jedis) jedis).close();
}
}
}
private JedisCommands getResource() throws JobPersistenceException {
if (jedisCluster != null) {
return jedisCluster;
} else {
return jedisPool.getResource();
}
}
private interface LockCallback<T> {
T doWithLock(JedisCommands jedis) throws JobPersistenceException;
}
private abstract class LockCallbackWithoutResult implements LockCallback<Void> {}
public void setLockTimeout(int lockTimeout) {
this.lockTimeout = lockTimeout;
}
public void setLockTimeout(String lockTimeout){
setLockTimeout(Integer.valueOf(lockTimeout));
}
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
public void setPort(String port){
setPort(Integer.valueOf(port));
}
public void setDatabase(int database){
this.database = (short) database;
}
public void setDatabase(String database){
setDatabase(Short.valueOf(database));
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
public void setKeyDelimiter(String keyDelimiter) {
this.keyDelimiter = keyDelimiter;
}
public void setRedisCluster(boolean clustered) {
this.redisCluster = clustered;
}
public void setRedisSentinel(boolean sentinel) {
this.redisSentinel = sentinel;
}
public void setMasterGroupName(String masterGroupName) {
this.masterGroupName = masterGroupName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
/**
* Inform the <code>JobStore</code> of the Scheduler instance's Id,
* prior to initialize being invoked.
*
* @param schedInstId the instanceid for the current scheduler
* @since 1.7
*/
@Override
public void setInstanceId(String schedInstId) {
instanceId = schedInstId;
}
/**
* Inform the <code>JobStore</code> of the Scheduler instance's name,
* prior to initialize being invoked.
*
* @param schedName the name of the current scheduler
* @since 1.7
*/
@Override
public void setInstanceName(String schedName) {
// nothing to do
}
/**
* Tells the JobStore the pool size used to execute jobs
*
* @param poolSize amount of threads allocated for job execution
* @since 2.0
*/
@Override
public void setThreadPoolSize(int poolSize) {
// nothing to do
}
private Set<HostAndPort> buildNodesSetFromHost() {
Set<HostAndPort> nodes = new HashSet<>();
for (String hostName : host.split(",")) {
int hostPort = port;
if (hostName.contains(":")) {
String[] parts = hostName.split(":");
hostName = parts[0];
hostPort = Integer.valueOf(parts[1]);
}
nodes.add(new HostAndPort(hostName, hostPort));
}
return nodes;
}
}