package net.joelinn.quartz.jobstore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.quartz.Calendar;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.impl.matchers.StringMatcher;
import org.quartz.spi.OperableTrigger;
import org.quartz.spi.SchedulerSignaler;
import org.quartz.spi.TriggerFiredBundle;
import org.quartz.spi.TriggerFiredResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
import java.util.*;
/**
* Joe Linn
* 8/22/2015
*/
public class RedisStorage extends AbstractRedisStorage<Jedis> {
private static final Logger logger = LoggerFactory.getLogger(RedisStorage.class);
public RedisStorage(RedisJobStoreSchema redisSchema, ObjectMapper mapper, SchedulerSignaler signaler, String schedulerInstanceId, int lockTimeout) {
super(redisSchema, mapper, signaler, schedulerInstanceId, lockTimeout);
}
/**
* Remove the given job from Redis
* @param jobKey the job to be removed
* @param jedis a thread-safe Redis connection
* @return true if the job was removed; false if it did not exist
*/
@Override
public boolean removeJob(JobKey jobKey, Jedis jedis) throws JobPersistenceException {
final String jobHashKey = redisSchema.jobHashKey(jobKey);
final String jobDataMapHashKey = redisSchema.jobDataMapHashKey(jobKey);
final String jobGroupSetKey = redisSchema.jobGroupSetKey(jobKey);
final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(jobKey);
Pipeline pipe = jedis.pipelined();
// remove the job and any associated data
Response<Long> delJobHashKeyResponse = pipe.del(jobHashKey);
pipe.del(jobDataMapHashKey);
// remove the job from the set of all jobs
pipe.srem(redisSchema.jobsSet(), jobHashKey);
// remove the job from its group
pipe.srem(jobGroupSetKey, jobHashKey);
// retrieve the keys for all triggers associated with this job, then delete that set
Response<Set<String>> jobTriggerSetResponse = pipe.smembers(jobTriggerSetKey);
pipe.del(jobTriggerSetKey);
Response<Long> jobGroupSetSizeResponse = pipe.scard(jobGroupSetKey);
pipe.sync();
if(jobGroupSetSizeResponse.get() == 0){
// The group now contains no jobs. Remove it from the set of all job groups.
jedis.srem(redisSchema.jobGroupsSet(), jobGroupSetKey);
}
// remove all triggers associated with this job
pipe = jedis.pipelined();
for (String triggerHashKey : jobTriggerSetResponse.get()) {
// get this trigger's TriggerKey
final TriggerKey triggerKey = redisSchema.triggerKey(triggerHashKey);
final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(triggerKey);
unsetTriggerState(triggerHashKey, jedis);
// remove the trigger from the set of all triggers
pipe.srem(redisSchema.triggersSet(), triggerHashKey);
// remove the trigger's group from the set of all trigger groups
pipe.srem(redisSchema.triggerGroupsSet(), triggerGroupSetKey);
// remove this trigger from its group
pipe.srem(triggerGroupSetKey, triggerHashKey);
// delete the trigger
pipe.del(triggerHashKey);
}
pipe.sync();
return delJobHashKeyResponse.get() == 1;
}
/**
* Remove (delete) the <code>{@link org.quartz.Trigger}</code> with the given key.
* @param triggerKey the key of the trigger to be removed
* @param removeNonDurableJob if true, the job associated with the given trigger will be removed if it is non-durable
* and has no other triggers
* @param jedis a thread-safe Redis connection
* @return true if the trigger was found and removed
*/
@Override
protected boolean removeTrigger(TriggerKey triggerKey, boolean removeNonDurableJob, Jedis jedis) throws JobPersistenceException, ClassNotFoundException {
final String triggerHashKey = redisSchema.triggerHashKey(triggerKey);
final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(triggerKey);
if(!jedis.exists(triggerHashKey)){
return false;
}
OperableTrigger trigger = retrieveTrigger(triggerKey, jedis);
final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey());
final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(trigger.getJobKey());
Pipeline pipe = jedis.pipelined();
// remove the trigger from the set of all triggers
pipe.srem(redisSchema.triggersSet(), triggerHashKey);
// remove the trigger from its trigger group set
pipe.srem(triggerGroupSetKey, triggerHashKey);
// remove the trigger from the associated job's trigger set
pipe.srem(jobTriggerSetKey, triggerHashKey);
pipe.sync();
if(jedis.scard(triggerGroupSetKey) == 0){
// The trigger group set is empty. Remove the trigger group from the set of trigger groups.
jedis.srem(redisSchema.triggerGroupsSet(), triggerGroupSetKey);
}
if(removeNonDurableJob){
pipe = jedis.pipelined();
Response<Long> jobTriggerSetKeySizeResponse = pipe.scard(jobTriggerSetKey);
Response<Boolean> jobExistsResponse = pipe.exists(jobHashKey);
pipe.sync();
if(jobTriggerSetKeySizeResponse.get() == 0 && jobExistsResponse.get()){
JobDetail job = retrieveJob(trigger.getJobKey(), jedis);
if(!job.isDurable()){
// Job is not durable and has no remaining triggers. Delete it.
removeJob(job.getKey(), jedis);
signaler.notifySchedulerListenersJobDeleted(job.getKey());
}
}
}
if(isNullOrEmpty(trigger.getCalendarName())){
jedis.srem(redisSchema.calendarTriggersSetKey(trigger.getCalendarName()), triggerHashKey);
}
unsetTriggerState(triggerHashKey, jedis);
jedis.del(triggerHashKey);
jedis.del(redisSchema.triggerDataMapHashKey(triggerKey));
return true;
}
/**
* Store a job in Redis
* @param jobDetail the {@link org.quartz.JobDetail} object to be stored
* @param replaceExisting if true, any existing job with the same group and name as the given job will be overwritten
* @param jedis a thread-safe Redis connection
* @throws org.quartz.ObjectAlreadyExistsException
*/
@Override
@SuppressWarnings("unchecked")
public void storeJob(JobDetail jobDetail, boolean replaceExisting, Jedis jedis) throws ObjectAlreadyExistsException {
final String jobHashKey = redisSchema.jobHashKey(jobDetail.getKey());
final String jobDataMapHashKey = redisSchema.jobDataMapHashKey(jobDetail.getKey());
final String jobGroupSetKey = redisSchema.jobGroupSetKey(jobDetail.getKey());
if(!replaceExisting && jedis.exists(jobHashKey)){
throw new ObjectAlreadyExistsException(jobDetail);
}
Pipeline pipe = jedis.pipelined();
pipe.hmset(jobHashKey, (Map<String, String>) mapper.convertValue(jobDetail, new TypeReference<HashMap<String, String>>() {}));
if(jobDetail.getJobDataMap() != null && !jobDetail.getJobDataMap().isEmpty()){
pipe.hmset(jobDataMapHashKey, getStringDataMap(jobDetail.getJobDataMap()));
}
pipe.sadd(redisSchema.jobsSet(), jobHashKey);
pipe.sadd(redisSchema.jobGroupsSet(), jobGroupSetKey);
pipe.sadd(jobGroupSetKey, jobHashKey);
pipe.sync();
}
/**
* Store a trigger in redis
* @param trigger the trigger to be stored
* @param replaceExisting true if an existing trigger with the same identity should be replaced
* @param jedis a thread-safe Redis connection
* @throws JobPersistenceException
* @throws ObjectAlreadyExistsException
*/
@Override
public void storeTrigger(OperableTrigger trigger, boolean replaceExisting, Jedis jedis) throws JobPersistenceException {
final String triggerHashKey = redisSchema.triggerHashKey(trigger.getKey());
final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(trigger.getKey());
final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(trigger.getJobKey());
if(!(trigger instanceof SimpleTrigger) && !(trigger instanceof CronTrigger)){
throw new UnsupportedOperationException("Only SimpleTrigger and CronTrigger are supported.");
}
final boolean exists = jedis.exists(triggerHashKey);
if(exists && !replaceExisting){
throw new ObjectAlreadyExistsException(trigger);
}
Map<String, String> triggerMap = mapper.convertValue(trigger, new TypeReference<HashMap<String, String>>() {});
triggerMap.put(TRIGGER_CLASS, trigger.getClass().getName());
Pipeline pipe = jedis.pipelined();
pipe.hmset(triggerHashKey, triggerMap);
pipe.sadd(redisSchema.triggersSet(), triggerHashKey);
pipe.sadd(redisSchema.triggerGroupsSet(), triggerGroupSetKey);
pipe.sadd(triggerGroupSetKey, triggerHashKey);
pipe.sadd(jobTriggerSetKey, triggerHashKey);
if(trigger.getCalendarName() != null && !trigger.getCalendarName().isEmpty()){
final String calendarTriggersSetKey = redisSchema.calendarTriggersSetKey(trigger.getCalendarName());
pipe.sadd(calendarTriggersSetKey, triggerHashKey);
}
if (trigger.getJobDataMap() != null && !trigger.getJobDataMap().isEmpty()) {
final String triggerDataMapHashKey = redisSchema.triggerDataMapHashKey(trigger.getKey());
pipe.hmset(triggerDataMapHashKey, getStringDataMap(trigger.getJobDataMap()));
}
pipe.sync();
if(exists){
// We're overwriting a previously stored instance of this trigger, so clear any existing trigger state.
unsetTriggerState(triggerHashKey, jedis);
}
pipe = jedis.pipelined();
Response<Boolean> triggerPausedResponse = pipe.sismember(redisSchema.pausedTriggerGroupsSet(), triggerGroupSetKey);
Response<Boolean> jobPausedResponse = pipe.sismember(redisSchema.pausedJobGroupsSet(), redisSchema.jobGroupSetKey(trigger.getJobKey()));
pipe.sync();
final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey());
final long nextFireTime = trigger.getNextFireTime() != null ? trigger.getNextFireTime().getTime() : -1;
if (triggerPausedResponse.get() || jobPausedResponse.get()){
if (jedis.sismember(redisSchema.blockedJobsSet(), jobHashKey)) {
setTriggerState(RedisTriggerState.PAUSED_BLOCKED, (double) nextFireTime, triggerHashKey, jedis);
} else {
setTriggerState(RedisTriggerState.PAUSED, (double) nextFireTime, triggerHashKey, jedis);
}
} else if(trigger.getNextFireTime() != null){
if (jedis.sismember(redisSchema.blockedJobsSet(), jobHashKey)) {
setTriggerState(RedisTriggerState.BLOCKED, nextFireTime, triggerHashKey, jedis);
} else {
setTriggerState(RedisTriggerState.WAITING, (double) trigger.getNextFireTime().getTime(), triggerHashKey, jedis);
}
}
}
/**
* Unsets the state of the given trigger key by removing the trigger from all trigger state sets.
* @param triggerHashKey the redis key of the desired trigger hash
* @param jedis a thread-safe Redis connection
* @return true if the trigger was removed, false if the trigger was stateless
* @throws org.quartz.JobPersistenceException if the unset operation failed
*/
@Override
public boolean unsetTriggerState(final String triggerHashKey, Jedis jedis) throws JobPersistenceException {
boolean removed = false;
Pipeline pipe = jedis.pipelined();
List<Response<Long>> responses = new ArrayList<>(RedisTriggerState.values().length);
for (RedisTriggerState state : RedisTriggerState.values()) {
responses.add(pipe.zrem(redisSchema.triggerStateKey(state), triggerHashKey));
}
pipe.sync();
for (Response<Long> response : responses) {
removed = response.get() == 1;
if(removed){
jedis.del(redisSchema.triggerLockKey(redisSchema.triggerKey(triggerHashKey)));
break;
}
}
return removed;
}
/**
* Store a {@link org.quartz.Calendar}
* @param name the name of the calendar
* @param calendar the calendar object to be stored
* @param replaceExisting if true, any existing calendar with the same name will be overwritten
* @param updateTriggers if true, any existing triggers associated with the calendar will be updated
* @param jedis a thread-safe Redis connection
* @throws JobPersistenceException
*/
@Override
public void storeCalendar(String name, Calendar calendar, boolean replaceExisting, boolean updateTriggers, Jedis jedis) throws JobPersistenceException{
final String calendarHashKey = redisSchema.calendarHashKey(name);
if(!replaceExisting && jedis.exists(calendarHashKey)){
throw new ObjectAlreadyExistsException(String.format("Calendar with key %s already exists.", calendarHashKey));
}
Map<String, String> calendarMap = new HashMap<>();
calendarMap.put(CALENDAR_CLASS, calendar.getClass().getName());
try {
calendarMap.put(CALENDAR_JSON, mapper.writeValueAsString(calendar));
} catch (JsonProcessingException e) {
throw new JobPersistenceException("Unable to serialize calendar.", e);
}
Pipeline pipe = jedis.pipelined();
pipe.hmset(calendarHashKey, calendarMap);
pipe.sadd(redisSchema.calendarsSet(), calendarHashKey);
pipe.sync();
if(updateTriggers){
final String calendarTriggersSetKey = redisSchema.calendarTriggersSetKey(name);
Set<String> triggerHashKeys = jedis.smembers(calendarTriggersSetKey);
for (String triggerHashKey : triggerHashKeys) {
OperableTrigger trigger = retrieveTrigger(redisSchema.triggerKey(triggerHashKey), jedis);
long removed = jedis.zrem(redisSchema.triggerStateKey(RedisTriggerState.WAITING), triggerHashKey);
trigger.updateWithNewCalendar(calendar, misfireThreshold);
if(removed == 1){
setTriggerState(RedisTriggerState.WAITING, (double) trigger.getNextFireTime().getTime(), triggerHashKey, jedis);
}
}
}
}
/**
* Remove (delete) the <code>{@link org.quartz.Calendar}</code> with the given name.
* @param calendarName the name of the calendar to be removed
* @param jedis a thread-safe Redis connection
* @return true if a calendar with the given name was found and removed
*/
@Override
public boolean removeCalendar(String calendarName, Jedis jedis) throws JobPersistenceException {
final String calendarTriggersSetKey = redisSchema.calendarTriggersSetKey(calendarName);
if(jedis.scard(calendarTriggersSetKey) > 0){
throw new JobPersistenceException(String.format("There are triggers pointing to calendar %s, so it cannot be removed.", calendarName));
}
final String calendarHashKey = redisSchema.calendarHashKey(calendarName);
Pipeline pipe = jedis.pipelined();
Response<Long> deleteResponse = pipe.del(calendarHashKey);
pipe.srem(redisSchema.calendarsSet(), calendarHashKey);
pipe.sync();
return deleteResponse.get() == 1;
}
/**
* Get the keys of all of the <code>{@link org.quartz.Job}</code> s that have the given group name.
* @param matcher the matcher with which to compare group names
* @param jedis a thread-safe Redis connection
* @return the set of all JobKeys which have the given group name
*/
@Override
public Set<JobKey> getJobKeys(GroupMatcher<JobKey> matcher, Jedis jedis){
Set<JobKey> jobKeys = new HashSet<>();
if(matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS){
final String jobGroupSetKey = redisSchema.jobGroupSetKey(new JobKey("", matcher.getCompareToValue()));
final Set<String> jobs = jedis.smembers(jobGroupSetKey);
if(jobs != null){
for (final String job : jobs) {
jobKeys.add(redisSchema.jobKey(job));
}
}
}
else{
List<Response<Set<String>>> jobGroups = new ArrayList<>();
Pipeline pipe = jedis.pipelined();
for (final String jobGroupSetKey : jedis.smembers(redisSchema.jobGroupsSet())) {
if(matcher.getCompareWithOperator().evaluate(redisSchema.jobGroup(jobGroupSetKey), matcher.getCompareToValue())){
jobGroups.add(pipe.smembers(jobGroupSetKey));
}
}
pipe.sync();
for (Response<Set<String>> jobGroup : jobGroups) {
if(jobGroup.get() != null){
for (final String job : jobGroup.get()) {
jobKeys.add(redisSchema.jobKey(job));
}
}
}
}
return jobKeys;
}
/**
* Get the names of all of the <code>{@link org.quartz.Trigger}</code> s that have the given group name.
* @param matcher the matcher with which to compare group names
* @param jedis a thread-safe Redis connection
* @return the set of all TriggerKeys which have the given group name
*/
@Override
public Set<TriggerKey> getTriggerKeys(GroupMatcher<TriggerKey> matcher, Jedis jedis){
Set<TriggerKey> triggerKeys = new HashSet<>();
if(matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS){
final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(new TriggerKey("", matcher.getCompareToValue()));
final Set<String> triggers = jedis.smembers(triggerGroupSetKey);
if(triggers != null){
for (final String trigger : triggers) {
triggerKeys.add(redisSchema.triggerKey(trigger));
}
}
}
else{
List<Response<Set<String>>> triggerGroups = new ArrayList<>();
Pipeline pipe = jedis.pipelined();
for (final String triggerGroupSetKey : jedis.smembers(redisSchema.triggerGroupsSet())) {
if(matcher.getCompareWithOperator().evaluate(redisSchema.triggerGroup(triggerGroupSetKey), matcher.getCompareToValue())){
triggerGroups.add(pipe.smembers(triggerGroupSetKey));
}
}
pipe.sync();
for (Response<Set<String>> triggerGroup : triggerGroups) {
if(triggerGroup.get() != null){
for (final String trigger : triggerGroup.get()) {
triggerKeys.add(redisSchema.triggerKey(trigger));
}
}
}
}
return triggerKeys;
}
/**
* Get the current state of the identified <code>{@link org.quartz.Trigger}</code>.
* @param triggerKey the key of the desired trigger
* @param jedis a thread-safe Redis connection
* @return the state of the trigger
*/
@Override
public Trigger.TriggerState getTriggerState(TriggerKey triggerKey, Jedis jedis){
final String triggerHashKey = redisSchema.triggerHashKey(triggerKey);
Pipeline pipe = jedis.pipelined();
Map<RedisTriggerState, Response<Double>> scores = new HashMap<>(RedisTriggerState.values().length);
for (RedisTriggerState redisTriggerState : RedisTriggerState.values()) {
scores.put(redisTriggerState, pipe.zscore(redisSchema.triggerStateKey(redisTriggerState), triggerHashKey));
}
pipe.sync();
for (Map.Entry<RedisTriggerState, Response<Double>> entry : scores.entrySet()) {
if(entry.getValue().get() != null){
return entry.getKey().getTriggerState();
}
}
return Trigger.TriggerState.NONE;
}
/**
* Pause the trigger with the given key
* @param triggerKey the key of the trigger to be paused
* @param jedis a thread-safe Redis connection
* @throws JobPersistenceException if the desired trigger does not exist
*/
@Override
public void pauseTrigger(TriggerKey triggerKey, Jedis jedis) throws JobPersistenceException {
final String triggerHashKey = redisSchema.triggerHashKey(triggerKey);
Pipeline pipe = jedis.pipelined();
Response<Boolean> exists = pipe.exists(triggerHashKey);
Response<Double> completedScore = pipe.zscore(redisSchema.triggerStateKey(RedisTriggerState.COMPLETED), triggerHashKey);
Response<String> nextFireTimeResponse = pipe.hget(triggerHashKey, TRIGGER_NEXT_FIRE_TIME);
Response<Double> blockedScore = pipe.zscore(redisSchema.triggerStateKey(RedisTriggerState.BLOCKED), triggerHashKey);
pipe.sync();
if(!exists.get()){
return;
}
if(completedScore.get() != null){
// doesn't make sense to pause a completed trigger
return;
}
final long nextFireTime = nextFireTimeResponse.get() == null
|| nextFireTimeResponse.get().isEmpty() ? -1 : Long.parseLong(nextFireTimeResponse.get());
if(blockedScore.get() != null){
setTriggerState(RedisTriggerState.PAUSED_BLOCKED, (double) nextFireTime, triggerHashKey, jedis);
}
else{
setTriggerState(RedisTriggerState.PAUSED, (double) nextFireTime, triggerHashKey, jedis);
}
}
/**
* Pause all of the <code>{@link org.quartz.Trigger}s</code> in the given group.
* @param matcher matcher for the trigger groups to be paused
* @param jedis a thread-safe Redis connection
* @return a collection of names of trigger groups which were matched and paused
* @throws JobPersistenceException
*/
@Override
public Collection<String> pauseTriggers(GroupMatcher<TriggerKey> matcher, Jedis jedis) throws JobPersistenceException {
Set<String> pausedTriggerGroups = new HashSet<>();
if(matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS){
final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(new TriggerKey("", matcher.getCompareToValue()));
final long addResult = jedis.sadd(redisSchema.pausedTriggerGroupsSet(), triggerGroupSetKey);
if(addResult > 0){
for (final String trigger : jedis.smembers(triggerGroupSetKey)) {
pauseTrigger(redisSchema.triggerKey(trigger), jedis);
}
pausedTriggerGroups.add(redisSchema.triggerGroup(triggerGroupSetKey));
}
}
else{
Map<String, Response<Set<String>>> triggerGroups = new HashMap<>();
Pipeline pipe = jedis.pipelined();
for (final String triggerGroupSetKey : jedis.smembers(redisSchema.triggerGroupsSet())) {
if(matcher.getCompareWithOperator().evaluate(redisSchema.triggerGroup(triggerGroupSetKey), matcher.getCompareToValue())){
triggerGroups.put(triggerGroupSetKey, pipe.smembers(triggerGroupSetKey));
}
}
pipe.sync();
for (final Map.Entry<String, Response<Set<String>>> entry : triggerGroups.entrySet()) {
if(jedis.sadd(redisSchema.pausedJobGroupsSet(), entry.getKey()) > 0){
// This trigger group was not paused. Pause it now.
pausedTriggerGroups.add(redisSchema.triggerGroup(entry.getKey()));
for (final String triggerHashKey : entry.getValue().get()) {
pauseTrigger(redisSchema.triggerKey(triggerHashKey), jedis);
}
}
}
}
return pausedTriggerGroups;
}
/**
* Pause all of the <code>{@link org.quartz.Job}s</code> in the given group - by pausing all of their
* <code>Trigger</code>s.
* @param groupMatcher the mather which will determine which job group should be paused
* @param jedis a thread-safe Redis connection
* @return a collection of names of job groups which have been paused
* @throws JobPersistenceException
*/
@Override
public Collection<String> pauseJobs(GroupMatcher<JobKey> groupMatcher, Jedis jedis) throws JobPersistenceException {
Set<String> pausedJobGroups = new HashSet<>();
if(groupMatcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS){
final String jobGroupSetKey = redisSchema.jobGroupSetKey(new JobKey("", groupMatcher.getCompareToValue()));
if(jedis.sadd(redisSchema.pausedJobGroupsSet(), jobGroupSetKey) > 0){
pausedJobGroups.add(redisSchema.jobGroup(jobGroupSetKey));
for (String job : jedis.smembers(jobGroupSetKey)) {
pauseJob(redisSchema.jobKey(job), jedis);
}
}
}
else{
Map<String, Response<Set<String>>> jobGroups = new HashMap<>();
Pipeline pipe = jedis.pipelined();
for (final String jobGroupSetKey : jedis.smembers(redisSchema.jobGroupsSet())) {
if(groupMatcher.getCompareWithOperator().evaluate(redisSchema.jobGroup(jobGroupSetKey), groupMatcher.getCompareToValue())){
jobGroups.put(jobGroupSetKey, pipe.smembers(jobGroupSetKey));
}
}
pipe.sync();
for (final Map.Entry<String, Response<Set<String>>> entry : jobGroups.entrySet()) {
if(jedis.sadd(redisSchema.pausedJobGroupsSet(), entry.getKey()) > 0){
// This job group was not already paused. Pause it now.
pausedJobGroups.add(redisSchema.jobGroup(entry.getKey()));
for (final String jobHashKey : entry.getValue().get()) {
pauseJob(redisSchema.jobKey(jobHashKey), jedis);
}
}
}
}
return pausedJobGroups;
}
/**
* Resume (un-pause) a {@link org.quartz.Trigger}
* @param triggerKey the key of the trigger to be resumed
* @param jedis a thread-safe Redis connection
*/
@Override
public void resumeTrigger(TriggerKey triggerKey, Jedis jedis) throws JobPersistenceException {
final String triggerHashKey = redisSchema.triggerHashKey(triggerKey);
Pipeline pipe = jedis.pipelined();
Response<Boolean> exists = pipe.sismember(redisSchema.triggersSet(), triggerHashKey);
Response<Double> isPaused = pipe.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED), triggerHashKey);
Response<Double> isPausedBlocked = pipe.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED_BLOCKED), triggerHashKey);
pipe.sync();
if(!exists.get()){
// Trigger does not exist. Nothing to do.
return;
}
if(isPaused.get() == null && isPausedBlocked.get() == null){
// Trigger is not paused. Nothing to do.
return;
}
OperableTrigger trigger = retrieveTrigger(triggerKey, jedis);
final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey());
final Date nextFireTime = trigger.getNextFireTime();
if(nextFireTime != null){
if(jedis.sismember(redisSchema.blockedJobsSet(), jobHashKey)){
setTriggerState(RedisTriggerState.BLOCKED, (double) nextFireTime.getTime(), triggerHashKey, jedis);
}
else{
setTriggerState(RedisTriggerState.WAITING, (double) nextFireTime.getTime(), triggerHashKey, jedis);
}
}
applyMisfire(trigger, jedis);
}
/**
* Resume (un-pause) all of the <code>{@link org.quartz.Trigger}s</code> in the given group.
* @param matcher matcher for the trigger groups to be resumed
* @param jedis a thread-safe Redis connection
* @return the names of trigger groups which were resumed
*/
@Override
public Collection<String> resumeTriggers(GroupMatcher<TriggerKey> matcher, Jedis jedis) throws JobPersistenceException {
Set<String> resumedTriggerGroups = new HashSet<>();
if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) {
final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(new TriggerKey("", matcher.getCompareToValue()));
Pipeline pipe = jedis.pipelined();
pipe.srem(redisSchema.pausedJobGroupsSet(), triggerGroupSetKey);
Response<Set<String>> triggerHashKeysResponse = pipe.smembers(triggerGroupSetKey);
pipe.sync();
for (String triggerHashKey : triggerHashKeysResponse.get()) {
OperableTrigger trigger = retrieveTrigger(redisSchema.triggerKey(triggerHashKey), jedis);
resumeTrigger(trigger.getKey(), jedis);
resumedTriggerGroups.add(trigger.getKey().getGroup());
}
}
else {
for (final String triggerGroupSetKey : jedis.smembers(redisSchema.triggerGroupsSet())) {
if(matcher.getCompareWithOperator().evaluate(redisSchema.triggerGroup(triggerGroupSetKey), matcher.getCompareToValue())){
resumedTriggerGroups.addAll(resumeTriggers(GroupMatcher.triggerGroupEquals(redisSchema.triggerGroup(triggerGroupSetKey)), jedis));
}
}
}
return resumedTriggerGroups;
}
/**
* Resume (un-pause) all of the <code>{@link org.quartz.Job}s</code> in the given group.
* @param matcher the matcher with which to compare job group names
* @param jedis a thread-safe Redis connection
* @return the set of job groups which were matched and resumed
*/
@Override
public Collection<String> resumeJobs(GroupMatcher<JobKey> matcher, Jedis jedis) throws JobPersistenceException {
Set<String> resumedJobGroups = new HashSet<>();
if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) {
final String jobGroupSetKey = redisSchema.jobGroupSetKey(new JobKey("", matcher.getCompareToValue()));
Pipeline pipe = jedis.pipelined();
Response<Long> unpauseResponse = pipe.srem(redisSchema.pausedJobGroupsSet(), jobGroupSetKey);
Response<Set<String>> jobsResponse = pipe.smembers(jobGroupSetKey);
pipe.sync();
if(unpauseResponse.get() > 0){
resumedJobGroups.add(redisSchema.jobGroup(jobGroupSetKey));
}
for (String job : jobsResponse.get()) {
resumeJob(redisSchema.jobKey(job), jedis);
}
}
else{
for (final String jobGroupSetKey : jedis.smembers(redisSchema.jobGroupsSet())) {
if(matcher.getCompareWithOperator().evaluate(redisSchema.jobGroup(jobGroupSetKey), matcher.getCompareToValue())){
resumedJobGroups.addAll(resumeJobs(GroupMatcher.jobGroupEquals(redisSchema.jobGroup(jobGroupSetKey)), jedis));
}
}
}
return resumedJobGroups;
}
/**
* 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
* @param jedis a thread-safe Redis connection
* @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(List<OperableTrigger> triggers, Jedis jedis) throws JobPersistenceException, ClassNotFoundException {
List<TriggerFiredResult> results = new ArrayList<>();
for (OperableTrigger trigger : triggers) {
final String triggerHashKey = redisSchema.triggerHashKey(trigger.getKey());
logger.debug(String.format("Trigger %s fired.", triggerHashKey));
Pipeline pipe = jedis.pipelined();
Response<Boolean> triggerExistsResponse = pipe.exists(triggerHashKey);
Response<Double> triggerAcquiredResponse = pipe.zscore(redisSchema.triggerStateKey(RedisTriggerState.ACQUIRED), triggerHashKey);
pipe.sync();
if(!triggerExistsResponse.get() || triggerAcquiredResponse.get() == null){
// the trigger does not exist or the trigger is not acquired
if(!triggerExistsResponse.get()){
logger.debug(String.format("Trigger %s does not exist.", triggerHashKey));
}
else{
logger.debug(String.format("Trigger %s was not acquired.", triggerHashKey));
}
continue;
}
Calendar calendar = null;
final String calendarName = trigger.getCalendarName();
if(calendarName != null){
calendar = retrieveCalendar(calendarName, jedis);
if(calendar == null){
continue;
}
}
final Date previousFireTime = trigger.getPreviousFireTime();
trigger.triggered(calendar);
// set the trigger state to WAITING
final Date nextFireDate = trigger.getNextFireTime();
long nextFireTime = 0;
if (nextFireDate != null) {
nextFireTime = nextFireDate.getTime();
jedis.hset(triggerHashKey, TRIGGER_NEXT_FIRE_TIME, Long.toString(nextFireTime));
setTriggerState(RedisTriggerState.WAITING, (double) nextFireTime, triggerHashKey, jedis);
}
JobDetail job = retrieveJob(trigger.getJobKey(), jedis);
TriggerFiredBundle triggerFiredBundle = new TriggerFiredBundle(job, trigger, calendar, false, new Date(), previousFireTime, previousFireTime, nextFireDate);
// handling jobs for which concurrent execution is disallowed
if (isJobConcurrentExecutionDisallowed(job.getJobClass())){
if (logger.isTraceEnabled()) {
logger.trace("Firing trigger " + trigger.getKey() + " for job " + job.getKey() + " for which concurrent execution is disallowed. Adding job to blocked jobs set.");
}
final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey());
final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(job.getKey());
for (String nonConcurrentTriggerHashKey : jedis.smembers(jobTriggerSetKey)) {
Double score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.WAITING), nonConcurrentTriggerHashKey);
if(score != null){
if (logger.isTraceEnabled()) {
logger.trace("Setting state of trigger " + trigger.getKey() + " for non-concurrent job " + job.getKey() + " to BLOCKED.");
}
setTriggerState(RedisTriggerState.BLOCKED, score, nonConcurrentTriggerHashKey, jedis);
}
else{
score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED), nonConcurrentTriggerHashKey);
if(score != null){
if (logger.isTraceEnabled()) {
logger.trace("Setting state of trigger " + trigger.getKey() + " for non-concurrent job " + job.getKey() + " to PAUSED_BLOCKED.");
}
setTriggerState(RedisTriggerState.PAUSED_BLOCKED, score, nonConcurrentTriggerHashKey, jedis);
}
}
}
pipe = jedis.pipelined();
pipe.set(redisSchema.jobBlockedKey(job.getKey()), schedulerInstanceId);
pipe.sadd(redisSchema.blockedJobsSet(), jobHashKey);
pipe.sync();
} else if(nextFireDate != null){
jedis.hset(triggerHashKey, TRIGGER_NEXT_FIRE_TIME, Long.toString(nextFireTime));
logger.debug(String.format("Releasing trigger %s with next fire time %s. Setting state to WAITING.", triggerHashKey, nextFireTime));
setTriggerState(RedisTriggerState.WAITING, (double) nextFireTime, triggerHashKey, jedis);
} else {
jedis.hset(triggerHashKey, TRIGGER_NEXT_FIRE_TIME, "");
unsetTriggerState(triggerHashKey, jedis);
}
results.add(new TriggerFiredResult(triggerFiredBundle));
}
return results;
}
/**
* 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 trigger which was completed
* @param jobDetail the job which was completed
* @param triggerInstCode the status of the completed job
* @param jedis a thread-safe Redis connection
*/
@Override
public void triggeredJobComplete(OperableTrigger trigger, JobDetail jobDetail, Trigger.CompletedExecutionInstruction triggerInstCode, Jedis jedis) throws JobPersistenceException, ClassNotFoundException {
final String jobHashKey = redisSchema.jobHashKey(jobDetail.getKey());
final String jobDataMapHashKey = redisSchema.jobDataMapHashKey(jobDetail.getKey());
final String triggerHashKey = redisSchema.triggerHashKey(trigger.getKey());
logger.debug(String.format("Job %s completed.", jobHashKey));
if(jedis.exists(jobHashKey)) {
// job was not deleted during execution
Pipeline pipe;
if (isPersistJobDataAfterExecution(jobDetail.getJobClass())) {
// update the job data map
JobDataMap jobDataMap = jobDetail.getJobDataMap();
pipe = jedis.pipelined();
pipe.del(jobDataMapHashKey);
if (jobDataMap != null && !jobDataMap.isEmpty()) {
pipe.hmset(jobDataMapHashKey, getStringDataMap(jobDataMap));
}
pipe.sync();
}
if (isJobConcurrentExecutionDisallowed(jobDetail.getJobClass())) {
// unblock the job
pipe = jedis.pipelined();
pipe.srem(redisSchema.blockedJobsSet(), jobHashKey);
pipe.del(redisSchema.jobBlockedKey(jobDetail.getKey()));
pipe.sync();
final String jobTriggersSetKey = redisSchema.jobTriggersSetKey(jobDetail.getKey());
for (String nonConcurrentTriggerHashKey : jedis.smembers(jobTriggersSetKey)) {
Double score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.BLOCKED), nonConcurrentTriggerHashKey);
if (score != null) {
setTriggerState(RedisTriggerState.WAITING, score, nonConcurrentTriggerHashKey, jedis);
}
else {
score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED_BLOCKED), nonConcurrentTriggerHashKey);
if (score != null) {
setTriggerState(RedisTriggerState.PAUSED, score, nonConcurrentTriggerHashKey, jedis);
}
}
}
signaler.signalSchedulingChange(0L);
}
}
else{
// unblock the job, even if it has been deleted
jedis.srem(redisSchema.blockedJobsSet(), jobHashKey);
}
if(jedis.exists(triggerHashKey)){
// trigger was not deleted during job execution
if(triggerInstCode == Trigger.CompletedExecutionInstruction.DELETE_TRIGGER){
if(trigger.getNextFireTime() == null){
// double-check for possible reschedule within job execution, which would cancel the need to delete
if(isNullOrEmpty(jedis.hget(triggerHashKey, TRIGGER_NEXT_FIRE_TIME))){
removeTrigger(trigger.getKey(), jedis);
}
}
else{
removeTrigger(trigger.getKey(), jedis);
signaler.signalSchedulingChange(0L);
}
}
else if(triggerInstCode == Trigger.CompletedExecutionInstruction.SET_TRIGGER_COMPLETE){
setTriggerState(RedisTriggerState.COMPLETED, (double) System.currentTimeMillis(), triggerHashKey, jedis);
signaler.signalSchedulingChange(0L);
}
else if(triggerInstCode == Trigger.CompletedExecutionInstruction.SET_TRIGGER_ERROR){
logger.debug(String.format("Trigger %s set to ERROR state.", triggerHashKey));
final double score = trigger.getNextFireTime() != null ? (double) trigger.getNextFireTime().getTime() : 0;
setTriggerState(RedisTriggerState.ERROR, score, triggerHashKey, jedis);
signaler.signalSchedulingChange(0L);
}
else if(triggerInstCode == Trigger.CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR){
final String jobTriggersSetKey = redisSchema.jobTriggersSetKey(jobDetail.getKey());
for (String errorTriggerHashKey : jedis.smembers(jobTriggersSetKey)) {
final String nextFireTime = jedis.hget(errorTriggerHashKey, TRIGGER_NEXT_FIRE_TIME);
final double score = isNullOrEmpty(nextFireTime) ? 0 : Double.parseDouble(nextFireTime);
setTriggerState(RedisTriggerState.ERROR, score, errorTriggerHashKey, jedis);
}
signaler.signalSchedulingChange(0L);
}
else if(triggerInstCode == Trigger.CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_COMPLETE){
final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(jobDetail.getKey());
for (String completedTriggerHashKey : jedis.smembers(jobTriggerSetKey)) {
setTriggerState(RedisTriggerState.COMPLETED, (double) System.currentTimeMillis(), completedTriggerHashKey, jedis);
}
signaler.signalSchedulingChange(0L);
}
}
}
}