'
--------------------------------------------------------------------------------
/src/main/java/com/redislabs/quartz/RedisJobStore.java:
--------------------------------------------------------------------------------
1 | package com.redislabs.quartz;
2 |
3 | import com.github.jedis.lock.JedisLock;
4 | import com.google.gson.Gson;
5 | import java.io.BufferedReader;
6 | import java.io.File;
7 | import java.io.FileInputStream;
8 | import java.io.FileOutputStream;
9 | import java.io.IOException;
10 | import java.io.InputStreamReader;
11 | import java.io.OutputStreamWriter;
12 | import java.io.Reader;
13 | import java.io.Writer;
14 | import java.text.ParseException;
15 | import java.util.ArrayList;
16 | import java.util.Collection;
17 | import java.util.Date;
18 | import java.util.HashMap;
19 | import java.util.HashSet;
20 | import java.util.List;
21 | import java.util.Map;
22 | import java.util.Map.Entry;
23 | import java.util.Set;
24 | import org.quartz.Calendar;
25 | import org.quartz.CronTrigger;
26 | import org.quartz.DisallowConcurrentExecution;
27 | import org.quartz.Job;
28 | import org.quartz.JobBuilder;
29 | import org.quartz.JobDataMap;
30 | import org.quartz.JobDetail;
31 | import org.quartz.JobKey;
32 | import org.quartz.JobPersistenceException;
33 | import org.quartz.ObjectAlreadyExistsException;
34 | import org.quartz.PersistJobDataAfterExecution;
35 | import org.quartz.SchedulerConfigException;
36 | import org.quartz.SchedulerException;
37 | import org.quartz.SimpleTrigger;
38 | import org.quartz.Trigger;
39 | import org.quartz.Trigger.CompletedExecutionInstruction;
40 | import org.quartz.Trigger.TriggerState;
41 | import org.quartz.TriggerKey;
42 | import org.quartz.impl.matchers.GroupMatcher;
43 | import org.quartz.impl.matchers.StringMatcher.StringOperatorName;
44 | import org.quartz.impl.triggers.CronTriggerImpl;
45 | import org.quartz.impl.triggers.SimpleTriggerImpl;
46 | import org.quartz.spi.ClassLoadHelper;
47 | import org.quartz.spi.JobStore;
48 | import org.quartz.spi.OperableTrigger;
49 | import org.quartz.spi.SchedulerSignaler;
50 | import org.quartz.spi.TriggerFiredBundle;
51 | import org.quartz.spi.TriggerFiredResult;
52 | import org.quartz.utils.ClassUtils;
53 | import org.slf4j.Logger;
54 | import org.slf4j.LoggerFactory;
55 | import redis.clients.jedis.Jedis;
56 | import redis.clients.jedis.JedisPool;
57 | import redis.clients.jedis.JedisPoolConfig;
58 | import redis.clients.jedis.JedisPubSub;
59 | import redis.clients.jedis.Pipeline;
60 | import redis.clients.jedis.Protocol;
61 | import redis.clients.jedis.Tuple;
62 |
63 | /**
64 | *
65 | * This class implements a {@link org.quartz.spi.JobStore}
that
66 | * utilizes Redis as a persistent storage.
67 | *
68 | *
69 | * @author Matan Kehat
70 | */
71 | public class RedisJobStore implements JobStore {
72 |
73 | /*** Sets Keys ***/
74 | private static final String JOBS_SET = "jobs";
75 | private static final String JOB_GROUPS_SET = "job_groups";
76 | private static final String BLOCKED_JOBS_SET = "blocked_jobs";
77 | private static final String TRIGGERS_SET = "triggers";
78 | private static final String TRIGGER_GROUPS_SET = "trigger_groups";
79 | private static final String CALENDARS_SET = "calendars";
80 | private static final String PAUSED_TRIGGER_GROUPS_SET = "paused__trigger_groups";
81 | private static final String PAUSED_JOB_GROUPS_SET = "paused_job_groups";
82 |
83 | /*** Job Hash Fields ***/
84 | private static final String JOB_CLASS = "job_class_name";
85 | private static final String DESCRIPTION = "description";
86 | private static final String IS_DURABLE = "is_durable";
87 | private static final String BLOCKED_BY = "blocked_by";
88 | private static final String BLOCK_TIME = "block_time";
89 |
90 | /*** Trigger Hash Fields ***/
91 | private static final String JOB_HASH_KEY = "job_hash_key";
92 | private static final String NEXT_FIRE_TIME = "next_fire_time";
93 | private static final String PREV_FIRE_TIME = "prev_fire_time";
94 | private static final String PRIORITY = "priority";
95 | private static final String TRIGGER_TYPE = "trigger_type";
96 | private static final String CALENDAR_NAME = "calendar_name";
97 | private static final String START_TIME = "start_time";
98 | private static final String END_TIME = "end_time";
99 | private static final String FINAL_FIRE_TIME = "final_fire_time";
100 | private static final String FIRE_INSTANCE_ID = "fire_instance_id";
101 | private static final String MISFIRE_INSTRUCTION = "misfire_instruction";
102 | private static final String LOCKED_BY = "locked_by";
103 | private static final String LOCK_TIME = "lock_time";
104 |
105 | /*** Simple Trigger Fields ***/
106 | private static final String TRIGGER_TYPE_SIMPLE = "SIMPLE";
107 | private static final String REPEAT_COUNT = "repeat_count";
108 | private static final String REPEAT_INTERVAL = "repeat_interval";
109 | private static final String TIMES_TRIGGERED = "times_triggered";
110 |
111 | /*** Cron Trigger Fields ***/
112 | private static final String TRIGGER_TYPE_CRON = "CRON";
113 | private static final String CRON_EXPRESSION = "cron_expression";
114 | private static final String TIME_ZONE_ID = "time_zone_id";
115 |
116 | /*** Calendar Hash Fields ***/
117 | private static final String CALENDAR_CLASS = "calendar_class";
118 | private static final String CALENDAR_SERIALIZED = "calendar_serialized";
119 |
120 | /*** Misc Keys ***/
121 | private static final String LAST_TRIGGERS_RELEASE_TIME = "last_triggers_release_time";
122 | private static final String LOCK = "lock";
123 |
124 | /*** Channels ***/
125 | private static final String UNLOCK_NOTIFICATIONS_CHANNEL = "unlock-notificactions";
126 |
127 | /*** Class Members ***/
128 | private final Logger log = LoggerFactory.getLogger(getClass());
129 | private static JedisPool pool;
130 | private static JedisLock lockPool;
131 | private ClassLoadHelper loadHelper;
132 | private SchedulerSignaler signaler;
133 | private String instanceId;
134 | private String host;
135 | private int port;
136 | private String password;
137 | private String instanceIdFilePath;
138 | private int releaseTriggersInterval; // In seconds
139 | private int lockTimeout;
140 |
141 | protected long misfireThreshold = 60000L;
142 |
143 |
144 | @Override
145 | public void initialize(ClassLoadHelper loadHelper,
146 | SchedulerSignaler signaler) throws SchedulerConfigException {
147 |
148 | this.loadHelper = loadHelper;
149 | this.signaler = signaler;
150 |
151 | // initializing a connection pool
152 | JedisPoolConfig config = new JedisPoolConfig();
153 | if (password != null)
154 | pool = new JedisPool(config, host, port, Protocol.DEFAULT_TIMEOUT, password);
155 | else
156 | pool = new JedisPool(config, host, port, Protocol.DEFAULT_TIMEOUT);
157 |
158 | // initializing a locking connection pool with a longer timeout
159 | if (lockTimeout == 0)
160 | lockTimeout = 10 * 60 * 1000; // 10 Minutes locking timeout
161 |
162 | lockPool = new JedisLock(pool.getResource(), "JobLock", lockTimeout);
163 |
164 | }
165 |
166 | @Override
167 | public void schedulerStarted() throws SchedulerException {
168 | if (this.instanceIdFilePath != null) {
169 | String path = this.instanceIdFilePath + File.separator + "scheduler.id";
170 | File instanceIdFile = new File(path);
171 |
172 | // releasing triggers that a previous scheduler instance didn't release
173 | String lockedByInstanceId = readInstanceId(instanceIdFile);
174 | if (lockedByInstanceId != null) {
175 | releaseLockedTriggers(lockedByInstanceId, RedisTriggerState.ACQUIRED, RedisTriggerState.WAITING);
176 | releaseLockedTriggers(lockedByInstanceId, RedisTriggerState.BLOCKED, RedisTriggerState.WAITING);
177 | releaseLockedTriggers(lockedByInstanceId, RedisTriggerState.PAUSED_BLOCKED, RedisTriggerState.PAUSED);
178 |
179 | releaseBlockedJobs(lockedByInstanceId);
180 | }
181 |
182 | // writing the current instance id to a file
183 | writeInstanceId(instanceIdFile);
184 | }
185 | }
186 |
187 | @Override
188 | public void schedulerPaused() {
189 | // No-op
190 | }
191 |
192 | @Override
193 | public void schedulerResumed() {
194 | // No-op
195 | }
196 |
197 | @Override
198 | public void shutdown() {
199 | if (pool != null)
200 | pool.destroy();
201 | }
202 |
203 | @Override
204 | public boolean supportsPersistence() {
205 | return true;
206 | }
207 |
208 | @Override
209 | public long getEstimatedTimeToReleaseAndAcquireTrigger() {
210 | // varied
211 | return 100;
212 | }
213 |
214 | @Override
215 | public boolean isClustered() {
216 | return true;
217 | }
218 |
219 | @Override
220 | public void storeJobAndTrigger(JobDetail newJob, OperableTrigger newTrigger)
221 | throws ObjectAlreadyExistsException, JobPersistenceException {
222 | String jobHashKey = createJobHashKey(newJob.getKey().getGroup(), newJob.getKey().getName());
223 | String triggerHashKey = createTriggerHashKey(newTrigger.getKey().getGroup(), newTrigger.getKey().getName());
224 | try (Jedis jedis = pool.getResource()) {
225 | lockPool.acquire();
226 | storeJob(newJob, false, jedis);
227 | storeTrigger(newTrigger, false, jedis);
228 | } catch (ObjectAlreadyExistsException ex) {
229 | log.warn("could not store job: " + jobHashKey + " and trigger: " + triggerHashKey, ex);
230 | } catch (Exception ex) {
231 | log.error("could not store job: " + jobHashKey + " and trigger: " + triggerHashKey, ex);
232 | throw new JobPersistenceException(ex.getMessage(), ex.getCause());
233 | } finally {
234 | lockPool.release();
235 | }
236 | }
237 |
238 | @Override
239 | public void storeJob(JobDetail newJob, boolean replaceExisting) throws JobPersistenceException {
240 | String jobHashKey = createJobHashKey(newJob.getKey().getGroup(), newJob.getKey().getName());
241 | try (Jedis jedis = pool.getResource()) {
242 | lockPool.acquire();
243 | storeJob(newJob, replaceExisting, jedis);
244 | } catch (ObjectAlreadyExistsException ex) {
245 | log.warn(jobHashKey + " already exists", ex);
246 | throw ex;
247 | } catch (Exception ex) {
248 | log.error("could not store job: " + jobHashKey, ex);
249 | throw new JobPersistenceException(ex.getMessage(), ex.getCause());
250 | } finally {
251 | lockPool.release();
252 | }
253 | }
254 |
255 | /**
256 | * Stores job in redis.
257 | *
258 | * @param newJob the new job
259 | * @param replaceExisting the replace existing
260 | * @param jedis thread-safe redis connection
261 | * @throws ObjectAlreadyExistsException
262 | */
263 | private void storeJob(JobDetail newJob, boolean replaceExisting, Jedis jedis)
264 | throws ObjectAlreadyExistsException {
265 | String jobHashKey = createJobHashKey(newJob.getKey().getGroup(), newJob.getKey().getName());
266 | String jobDataMapHashKey = createJobDataMapHashKey(newJob.getKey().getGroup(), newJob.getKey().getName());
267 | String jobGroupSetKey = createJobGroupSetKey(newJob.getKey().getGroup());
268 |
269 | if (jedis.exists(jobHashKey) && !replaceExisting)
270 | throw new ObjectAlreadyExistsException(newJob);
271 |
272 | Map jobDetails = new HashMap<>();
273 | jobDetails.put(DESCRIPTION, newJob.getDescription() != null ? newJob.getDescription() : "");
274 | jobDetails.put(JOB_CLASS, newJob.getJobClass().getName());
275 | jobDetails.put(IS_DURABLE, Boolean.toString(newJob.isDurable()));
276 | jobDetails.put(BLOCKED_BY, "");
277 | jobDetails.put(BLOCK_TIME, "");
278 | jedis.hmset(jobHashKey, jobDetails);
279 |
280 | if (newJob.getJobDataMap() != null && !newJob.getJobDataMap().isEmpty())
281 | jedis.hmset(jobDataMapHashKey, getStringDataMap(newJob.getJobDataMap()));
282 |
283 | jedis.sadd(JOBS_SET, jobHashKey);
284 | jedis.sadd(JOB_GROUPS_SET, jobGroupSetKey);
285 | jedis.sadd(jobGroupSetKey, jobHashKey);
286 | }
287 |
288 | /**
289 | * Creates a job data map key in the form of: 'job_data_map:<job_group_name>:<job_name>'
290 | *
291 | * @param groupName the job group name
292 | * @param jobName the job name
293 | * @return the job data map key
294 | */
295 | private String createJobDataMapHashKey(String groupName, String jobName) {
296 | return "job_data_map:" + groupName + ":" + jobName;
297 | }
298 |
299 | /**
300 | * Creates a job group set key in the form of: 'job_group:<job_group_name>'
301 | *
302 | * @param groupName the group name
303 | * @return the job group set key
304 | */
305 | private String createJobGroupSetKey(String groupName) {
306 | return "job_group:" + groupName;
307 | }
308 |
309 | /**
310 | * Creates a job key in the form of: 'job:<job_group_name>:<job_name>'
311 | *
312 | * @param groupName the job group name
313 | * @param jobName the job name
314 | * @return the job key
315 | */
316 | private String createJobHashKey(String groupName, String jobName) {
317 | return "job:" + groupName + ":" + jobName;
318 | }
319 |
320 | /**
321 | * Creates a calendar hash key in the form of: 'calendar:<calendar_name>'
322 | *
323 | * @param calendarName the calendar name
324 | * @return the calendar key
325 | */
326 | private String createCalendarHashKey(String calendarName) {
327 | return "calendar:" + calendarName;
328 | }
329 |
330 | private Map getStringDataMap(JobDataMap jobDataMap) {
331 | Map stringDataMap = new HashMap<>();
332 | for (String key : jobDataMap.keySet())
333 | stringDataMap.put(key, jobDataMap.get(key).toString());
334 |
335 | return stringDataMap;
336 | }
337 |
338 | @Override
339 | public void storeJobsAndTriggers(
340 | Map> triggersAndJobs,
341 | boolean replace) throws ObjectAlreadyExistsException,
342 | JobPersistenceException {
343 | try (Jedis jedis = pool.getResource()) {
344 | lockPool.acquire();
345 |
346 | // verifying jobs and triggers don't exist
347 | if (!replace) {
348 | for (Entry> entry : triggersAndJobs.entrySet()) {
349 | String jobHashKey = createJobHashKey(entry.getKey().getKey().getGroup(), entry.getKey().getKey().getName());
350 | if (jedis.exists(jobHashKey))
351 | throw new ObjectAlreadyExistsException(entry.getKey());
352 |
353 | for (Trigger trigger : entry.getValue()) {
354 | String triggerHashKey = createTriggerHashKey(trigger.getKey().getGroup(), trigger.getKey().getName());
355 | if (jedis.exists(triggerHashKey))
356 | throw new ObjectAlreadyExistsException(trigger);
357 | }
358 | }
359 | }
360 |
361 | // adding jobs and triggers
362 | for (Entry> entry : triggersAndJobs.entrySet()) {
363 | storeJob(entry.getKey(), true, jedis);
364 | for (Trigger trigger: entry.getValue())
365 | storeTrigger((OperableTrigger) trigger, true, jedis);
366 | }
367 | } catch (Exception ex) {
368 | log.error("could not store jobs and triggers", ex);
369 | throw new JobPersistenceException(ex.getMessage(), ex.getCause());
370 | } finally {
371 | lockPool.release();
372 | }
373 | }
374 |
375 | @Override
376 | public boolean removeJob(JobKey jobKey) throws JobPersistenceException {
377 | boolean removed = false;
378 | String jobHashKey = createJobHashKey(jobKey.getGroup(), jobKey.getName());
379 | try (Jedis jedis = pool.getResource()) {
380 | lockPool.acquire();
381 | if (jedis.exists(jobHashKey)) {
382 | removeJob(jobKey, jedis);
383 | removed = true;
384 | }
385 | } catch (Exception ex) {
386 | log.error("could not remove job: " + jobHashKey, ex);
387 | throw new JobPersistenceException(ex.getMessage(), ex.getCause());
388 | } finally {
389 | lockPool.release();
390 | }
391 | return removed;
392 | }
393 |
394 | /**
395 | * Removes the job from redis.
396 | *
397 | * @param jobKey the job key
398 | * @param jedis thread-safe redis connection
399 | */
400 | private void removeJob(JobKey jobKey, Jedis jedis) {
401 | String jobHashKey = createJobHashKey(jobKey.getGroup(), jobKey.getName());
402 | String jobDataMapHashKey = createJobDataMapHashKey(jobKey.getGroup(), jobKey.getName());
403 | String jobGroupSetKey = createJobGroupSetKey(jobKey.getGroup());
404 |
405 | jedis.del(jobHashKey);
406 | jedis.del(jobDataMapHashKey);
407 | jedis.srem(JOBS_SET, jobHashKey);
408 | jedis.srem(BLOCKED_JOBS_SET, jobHashKey);
409 | jedis.srem(jobGroupSetKey, jobHashKey);
410 | if (jedis.scard(jobGroupSetKey) == 0) {
411 | jedis.srem(JOB_GROUPS_SET, jobGroupSetKey);
412 | }
413 | }
414 |
415 | @Override
416 | public boolean removeJobs(List jobKeys)
417 | throws JobPersistenceException {
418 | boolean removed = jobKeys.size() > 0;
419 | for (JobKey jobKey : jobKeys)
420 | removed = removed && removeJob(jobKey);
421 |
422 | return removed;
423 | }
424 |
425 | @Override
426 | public JobDetail retrieveJob(JobKey jobKey) throws JobPersistenceException {
427 | JobDetail jobDetail = null;
428 | String jobHashkey = createJobHashKey(jobKey.getGroup(), jobKey.getName());
429 | try (Jedis jedis = pool.getResource()) {
430 | lockPool.acquire();
431 | jobDetail = retrieveJob(jobKey, jedis);
432 | } catch (JobPersistenceException | ClassNotFoundException | InterruptedException ex) {
433 | log.error("could not retrieve job: " + jobHashkey, ex);
434 | throw new JobPersistenceException(ex.getMessage(), ex.getCause());
435 | } finally {
436 | lockPool.release();
437 | }
438 | return jobDetail;
439 | }
440 |
441 | /**
442 | * Retrieves job from redis.
443 | *
444 | * @param jobKey the job key
445 | * @param jedis thread-safe redis connection
446 | * @return the job detail
447 | * @throws JobPersistenceException
448 | */
449 | @SuppressWarnings("unchecked")
450 | private JobDetail retrieveJob(JobKey jobKey, Jedis jedis) throws JobPersistenceException, ClassNotFoundException {
451 | String jobHashkey = createJobHashKey(jobKey.getGroup(), jobKey.getName());
452 | String jobDataMapHashKey = createJobDataMapHashKey(jobKey.getGroup(), jobKey.getName());
453 | if (!jedis.exists(jobHashkey)) {
454 | log.warn("job: " + jobHashkey + " does not exist");
455 | return null;
456 | }
457 | Class jobClass = (Class) loadHelper.getClassLoader().loadClass(jedis.hget(jobHashkey, JOB_CLASS));
458 | JobBuilder jobBuilder = JobBuilder.newJob(jobClass)
459 | .withIdentity(jobKey)
460 | .withDescription(jedis.hget(jobHashkey, DESCRIPTION))
461 | .storeDurably(Boolean.getBoolean(jedis.hget(jobHashkey, IS_DURABLE)));
462 |
463 | Set jobDataMapFields = jedis.hkeys(jobDataMapHashKey);
464 | if (!jobDataMapFields.isEmpty()) {
465 | for (String jobDataMapField : jobDataMapFields)
466 | jobBuilder.usingJobData(jobDataMapField, jedis.hget(jobDataMapHashKey, jobDataMapField));
467 | }
468 |
469 | return jobBuilder.build();
470 | }
471 |
472 | @Override
473 | public void storeTrigger(OperableTrigger newTrigger, boolean replaceExisting)
474 | throws ObjectAlreadyExistsException, JobPersistenceException {
475 | String triggerHashKey = createTriggerHashKey(newTrigger.getKey().getGroup(), newTrigger.getKey().getName());
476 | try (Jedis jedis = pool.getResource()) {
477 | lockPool.acquire();
478 | storeTrigger(newTrigger, replaceExisting, jedis);
479 | } catch (ObjectAlreadyExistsException ex) {
480 | log.warn(triggerHashKey + " already exists", ex);
481 | throw ex;
482 | } catch (Exception ex) {
483 | log.error("could not store trigger: " + triggerHashKey, ex);
484 | throw new JobPersistenceException(ex.getMessage(), ex.getCause());
485 | } finally {
486 | lockPool.release();
487 | }
488 | }
489 |
490 | /**
491 | * Stores trigger in redis.
492 | *
493 | * @param newTrigger the new trigger
494 | * @param replaceExisting replace existing
495 | * @param jedis thread-safe redis connection
496 | * @throws JobPersistenceException
497 | * @throws ObjectAlreadyExistsException
498 | */
499 | private void storeTrigger(OperableTrigger newTrigger, boolean replaceExisting, Jedis jedis)
500 | throws JobPersistenceException {
501 | String triggerHashKey = createTriggerHashKey(newTrigger.getKey().getGroup(), newTrigger.getKey().getName());
502 | String triggerGroupSetKey = createTriggerGroupSetKey(newTrigger.getKey().getGroup());
503 | String jobHashkey = createJobHashKey(newTrigger.getJobKey().getGroup(), newTrigger.getJobKey().getName());
504 | String jobTriggerSetkey = createJobTriggersSetKey(newTrigger.getJobKey().getGroup(), newTrigger.getJobKey().getName());
505 |
506 | if (jedis.exists(triggerHashKey) && !replaceExisting) {
507 | ObjectAlreadyExistsException ex = new ObjectAlreadyExistsException(newTrigger);
508 | log.warn(ex.toString());
509 | }
510 | Map trigger = new HashMap<>();
511 | trigger.put(JOB_HASH_KEY, jobHashkey);
512 | trigger.put(DESCRIPTION, newTrigger.getDescription() != null ? newTrigger.getDescription() : "");
513 | trigger.put(NEXT_FIRE_TIME, newTrigger.getNextFireTime() != null ? Long.toString(newTrigger.getNextFireTime().getTime()) : "");
514 | trigger.put(PREV_FIRE_TIME, newTrigger.getPreviousFireTime() != null ? Long.toString(newTrigger.getPreviousFireTime().getTime()) : "");
515 | trigger.put(PRIORITY, Integer.toString(newTrigger.getPriority()));
516 | trigger.put(START_TIME, newTrigger.getStartTime() != null ? Long.toString(newTrigger.getStartTime().getTime()) : "");
517 | trigger.put(END_TIME, newTrigger.getEndTime() != null ? Long.toString(newTrigger.getEndTime().getTime()) : "");
518 | trigger.put(FINAL_FIRE_TIME, newTrigger.getFinalFireTime() != null ? Long.toString(newTrigger.getFinalFireTime().getTime()) : "");
519 | trigger.put(FIRE_INSTANCE_ID, newTrigger.getFireInstanceId() != null ? newTrigger.getFireInstanceId() : "");
520 | trigger.put(MISFIRE_INSTRUCTION, Integer.toString(newTrigger.getMisfireInstruction()));
521 | trigger.put(CALENDAR_NAME, newTrigger.getCalendarName() != null ? newTrigger.getCalendarName() : "");
522 | if (newTrigger instanceof SimpleTrigger) {
523 | trigger.put(TRIGGER_TYPE, TRIGGER_TYPE_SIMPLE);
524 | trigger.put(REPEAT_COUNT, Integer.toString(((SimpleTrigger) newTrigger).getRepeatCount()));
525 | trigger.put(REPEAT_INTERVAL, Long.toString(((SimpleTrigger) newTrigger).getRepeatInterval()));
526 | trigger.put(TIMES_TRIGGERED, Integer.toString(((SimpleTrigger) newTrigger).getTimesTriggered()));
527 | } else if (newTrigger instanceof CronTrigger) {
528 | trigger.put(TRIGGER_TYPE, TRIGGER_TYPE_CRON);
529 | trigger.put(CRON_EXPRESSION, ((CronTrigger) newTrigger).getCronExpression() != null ? ((CronTrigger) newTrigger).getCronExpression() : "");
530 | trigger.put(TIME_ZONE_ID, ((CronTrigger) newTrigger).getTimeZone().getID() != null ? ((CronTrigger) newTrigger).getTimeZone().getID() : "");
531 | } else { // other trigger types are not supported
532 | throw new UnsupportedOperationException();
533 | }
534 |
535 | jedis.hmset(triggerHashKey, trigger);
536 | jedis.sadd(TRIGGERS_SET, triggerHashKey);
537 | jedis.sadd(TRIGGER_GROUPS_SET, triggerGroupSetKey);
538 | jedis.sadd(triggerGroupSetKey, triggerHashKey);
539 | jedis.sadd(jobTriggerSetkey, triggerHashKey);
540 | if (newTrigger.getCalendarName() != null && !newTrigger.getCalendarName().isEmpty()) { // storing the trigger for calendar, if exists
541 | String calendarTriggersSetKey = createCalendarTriggersSetKey(newTrigger.getCalendarName());
542 | jedis.sadd(calendarTriggersSetKey, triggerHashKey);
543 | }
544 |
545 | if (jedis.sismember(PAUSED_TRIGGER_GROUPS_SET, triggerGroupSetKey) || jedis.sismember(PAUSED_JOB_GROUPS_SET, createJobGroupSetKey(newTrigger.getJobKey().getGroup()))) {
546 | long nextFireTime = newTrigger.getNextFireTime() != null ? newTrigger.getNextFireTime().getTime() : -1;
547 | if (jedis.sismember(BLOCKED_JOBS_SET, jobHashkey))
548 | setTriggerState(RedisTriggerState.PAUSED_BLOCKED, (double)nextFireTime, triggerHashKey);
549 | else
550 | setTriggerState(RedisTriggerState.PAUSED, (double)nextFireTime, triggerHashKey);
551 | } else if (newTrigger.getNextFireTime() != null) {
552 | setTriggerState(RedisTriggerState.WAITING, (double)newTrigger.getNextFireTime().getTime(), triggerHashKey);
553 | }
554 | }
555 |
556 | /**
557 | * Creates a calendar_triggers hash key in the form of:
558 | * 'calendar_triggers:<calendar_name>'
559 | *
560 | * @param calendarName the calendar name
561 | * @return the calendar_triggers set key
562 | */
563 | private String createCalendarTriggersSetKey(String calendarName) {
564 | return "calendar_triggers:" + calendarName;
565 | }
566 |
567 | /**
568 | * Creates a job_triggers set key in the form of:
569 | * 'job_triggers:<job_group_name>:<job_name>'
570 | *
571 | * @param jobGroupName the job group name
572 | * @param jobName the job name
573 | * @return the job_triggers set key
574 | */
575 | private String createJobTriggersSetKey(String jobGroupName, String jobName) {
576 | return "job_triggers:" + jobGroupName + ":" + jobName;
577 | }
578 |
579 | /**
580 | * Creates a trigger group hash key in the form of:
581 | * 'trigger_group:<trigger_group_name>'
582 | *
583 | * @param groupName the group name
584 | * @return the trigger group set key
585 | */
586 | private String createTriggerGroupSetKey(String groupName) {
587 | return "trigger_group:" + groupName;
588 | }
589 |
590 | /**
591 | * Creates a trigger hash key in the form of:
592 | * 'trigger:<trigger_group_name>:<trigger_name>'
593 | *
594 | * @param groupName the group name
595 | * @param triggerName the trigger name
596 | * @return the trigger hash key
597 | */
598 | private String createTriggerHashKey(String groupName, String triggerName) {
599 | return "trigger:" + groupName + ":" + triggerName;
600 | }
601 |
602 | @Override
603 | public boolean removeTrigger(TriggerKey triggerKey)
604 | throws JobPersistenceException {
605 | boolean removed = false;
606 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName());
607 | try (Jedis jedis = pool.getResource()) {
608 | lockPool.acquire();
609 | if (jedis.exists(triggerHashKey)) {
610 | removeTrigger(triggerKey, jedis);
611 | removed = true;
612 | }
613 | } catch (Exception ex) {
614 | log.error("could not remove trigger: " + triggerHashKey, ex);
615 | throw new JobPersistenceException(ex.getMessage(), ex.getCause());
616 | } finally {
617 | lockPool.release();
618 | }
619 | return removed;
620 | }
621 |
622 | /**
623 | * Removes the trigger from redis.
624 | *
625 | * @param triggerKey the trigger key
626 | * jedis thread-safe redis connection
627 | * @throws JobPersistenceException
628 | */
629 | private void removeTrigger(TriggerKey triggerKey, Jedis jedis)
630 | throws JobPersistenceException {
631 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName());
632 | String triggerGroupSetKey = createTriggerGroupSetKey(triggerKey.getGroup());
633 |
634 | jedis.srem(TRIGGERS_SET, triggerHashKey);
635 | jedis.srem(triggerGroupSetKey, triggerHashKey);
636 |
637 | String jobHashkey = jedis.hget(triggerHashKey, JOB_HASH_KEY);
638 | String jobTriggerSetkey = createJobTriggersSetKey(jobHashkey.split(":")[1], jobHashkey.split(":")[2]);
639 | jedis.srem(jobTriggerSetkey, triggerHashKey);
640 |
641 | if (jedis.scard(triggerGroupSetKey) == 0) {
642 | jedis.srem(TRIGGER_GROUPS_SET, triggerGroupSetKey);
643 | }
644 |
645 | // handling orphaned jobs
646 | if (jedis.scard(jobTriggerSetkey) == 0 && jedis.exists(jobHashkey)) {
647 | if (!Boolean.parseBoolean(jedis.hget(jobHashkey, IS_DURABLE))) {
648 | JobKey jobKey = new JobKey(jobHashkey.split(":")[2], jobHashkey.split(":")[1]);
649 | removeJob(jobKey, jedis);
650 | signaler.notifySchedulerListenersJobDeleted(jobKey);
651 | }
652 | }
653 |
654 | String calendarName = jedis.hget(triggerHashKey, CALENDAR_NAME);
655 | if (!calendarName.isEmpty()) {
656 | String calendarTriggersSetKey = createCalendarTriggersSetKey(calendarName);
657 | jedis.srem(calendarTriggersSetKey, triggerHashKey);
658 | }
659 |
660 | // removing trigger state
661 | unsetTriggerState(triggerHashKey);
662 | jedis.del(triggerHashKey);
663 | }
664 |
665 | /**
666 | * Sets a trigger state by adding the trigger to a relevant sorted set, using it's next fire time as the score.
667 | * The caller should handle synchronization.
668 | *
669 | * @param state the new state to be set
670 | * @param score the trigger's next fire time
671 | * @param triggerHashKey the trigger hash key
672 | * @return 1- if set, 0- if the trigger is already member of the state's sorted set and it's score was updated, -1- if setting state failed
673 | * @throws JobPersistenceException
674 | */
675 | private long setTriggerState(RedisTriggerState state, double score, String triggerHashKey) throws JobPersistenceException {
676 | long success = -1;
677 | try (Jedis jedis = pool.getResource()) {
678 | long removed = unsetTriggerState(triggerHashKey);
679 | if (state != null && removed >= 0)
680 | success = jedis.zadd(state.getKey(), score, triggerHashKey);
681 | } catch (Exception ex) {
682 | log.error("could not set state " + state + " for trigger: " + triggerHashKey);
683 | throw new JobPersistenceException(ex.getMessage(), ex.getCause());
684 | }
685 | return success;
686 | }
687 |
688 | /**
689 | * Unsets a state of a given trigger key by removing the trigger from the trigger state zsets.
690 | * The caller should handle synchronization.
691 | *
692 | * @param triggerHashKey the trigger hash key
693 | * @param jedis the redis client
694 | * @return 1- if removed, 0- if the trigger was stateless, -1- if unset state failed
695 | * @throws JobPersistenceException
696 | */
697 | private long unsetTriggerState(String triggerHashKey) throws JobPersistenceException {
698 | Long removed = -1L;
699 | try (Jedis jedis = pool.getResource()) {
700 | Pipeline p = jedis.pipelined();
701 | p.zrem(RedisTriggerState.WAITING.getKey(), triggerHashKey);
702 | p.zrem(RedisTriggerState.PAUSED.getKey(), triggerHashKey);
703 | p.zrem(RedisTriggerState.BLOCKED.getKey(), triggerHashKey);
704 | p.zrem(RedisTriggerState.PAUSED_BLOCKED.getKey(), triggerHashKey);
705 | p.zrem(RedisTriggerState.ACQUIRED.getKey(), triggerHashKey);
706 | p.zrem(RedisTriggerState.COMPLETED.getKey(), triggerHashKey);
707 | p.zrem(RedisTriggerState.ERROR.getKey(), triggerHashKey);
708 | List