├── .gitignore ├── LICENSE.txt ├── pom.xml ├── readme.md ├── schema └── schema.txt └── src └── main └── java └── com └── redislabs └── quartz ├── RedisJobStore.java └── RedisTriggerState.java /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | nb-configuration.xml 3 | manifest.mf 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Redis Labs Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.redislabs.quartz 5 | redis-quartz 6 | 1.0-SNAPSHOT 7 | jar 8 | redis-quartz 9 | 10 | 11 | UTF-8 12 | 1.7 13 | 1.7 14 | 15 | 2.2.2 16 | 2.8.0 17 | 1.0.0 18 | 19 | 2.2.4 20 | 21 | 22 | 23 | 24 | org.quartz-scheduler 25 | quartz 26 | ${version.org.quartz-scheduler.quartz} 27 | 28 | 29 | redis.clients 30 | jedis 31 | ${version.redis.clients.jedis} 32 | jar 33 | 34 | 35 | com.google.code.gson 36 | gson 37 | ${version.com.google.code.gson.gson} 38 | 39 | 40 | com.github.jedis-lock 41 | jedis-lock 42 | ${version.com.github.jedis-lock.jedis-lock} 43 | 44 | 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # RedisJobStore 2 | 3 | A [Quartz Scheduler](http://quartz-scheduler.org/) JobStore that uses [Redis](http://redis.io/) for persistent storage. 4 | 5 | ## Configuration 6 | 7 | To get [Quartz](http://quartz-scheduler.org/) up and running quickly with `RedisJobStore`, use the following example to configure your `quartz.properties` file: 8 | 9 | # setting the scheduler's misfire threshold, in milliseconds 10 | org.quartz.jobStore.misfireThreshold: 60000 11 | 12 | # setting the scheduler's JobStore to RedisJobStore 13 | org.quartz.jobStore.class: com.redislabs.quartz.RedisJobStore 14 | 15 | # setting your redis host 16 | org.quartz.jobStore.host: 17 | 18 | # setting your redis port 19 | org.quartz.jobStore.port: 20 | 21 | # setting your redis password (optional) 22 | org.quartz.jobStore.password: 23 | 24 | # setting a 'releaseTriggersInterval' will trigger a mechanism for releasing triggers of non-alive schedulers in a given interval, in milliseconds 25 | org.quartz.jobStore.releaseTriggersInterval: 600000 26 | 27 | # setting a 'instanceIdFilePath' will release triggers of previous schedulers on startup 28 | org.quartz.jobStore.instanceIdFilePath: /etc/quartz 29 | 30 | 31 | ## External Libraries 32 | 33 | `RedisJobStore` uses the [jedis](https://github.com/xetorthio/jedis), [gson](https://code.google.com/p/google-gson/) and [jedis-lock](https://github.com/abelaska/jedis-lock) libraries, so you'll have to download them and add them to your project's classpath or define the relevant Maven dependencies: 34 | 35 | 36 | redis.clients 37 | jedis 38 | 2.0.0 39 | 40 | 41 | 42 | com.google.code.gson 43 | gson 44 | 2.2.4 45 | 46 | 47 | 48 | com.github.jedis-lock 49 | jedis-lock 50 | 1.0.0 51 | 52 | 53 | ## Limitations 54 | 55 | `RedisJobStore` attempts to be fully compliant with all of [Quartz](http://quartz-scheduler.org/)'s features, but currently has some limitations that you should be aware of: 56 | 57 | * Only `SimpleTrigger` and `CronTrigger`are supported. 58 | * For any `GroupMatcher`, only a `StringOperatorName.EQUALS` operator is supported. You should note that if your scheduler is designed to compare any group of jobs, triggers, etc. with a pattern-based matcher. 59 | * `RedisJobStore` is designed to use multiple schedulers, but it is not making any use of the `org.quartz.scheduler.instanceName`. The only limitation here is that you should maintain the uniquness of your trigger_group_name:trigger_name, and your job_group_name:job_name and you'll be good to go with multiple schedulers. 60 | * A `Scheduler` should be started once on a machine, also to ensure releasing locked triggers of previously crashed schedulers. 61 | * Data atomicity- `RedisJobStore` is not using any transaction-like mechanism, but ensures synchronization with global lockings. As a result, if a connection issue occurs during an operation, it might be partially completed. 62 | * `JobDataMap` values are stored and returned as Strings, so you should implement your jobs accordingly. 63 | * `RedisJobStore` is firing triggers only by their fire time, without any cosideration to their priorities at all. 64 | 65 | ## Known Issues 66 | 67 | 1. Quartz's standard JobStores are sometimes considering triggers without a next fire time as tirggers in a WAITING state. As `RedisJobStore` is using redis [Sorted Sets](http://redis.io/topics/data-types#sorted-sets) to maintain triggers states, using their next fire time as the score, it will consider these triggers as stateless. 68 | 69 | ## Redis Schema 70 | 71 | To better understand the workflow and the behavior of a [Quartz Scheduler](http://quartz-scheduler.org/) using a `RedisJobStore`, you may want to review the [redis](http://redis.io/) schema in which the `RedisJobStore` is making a use of at: [/schema/schema.txt](/schema/schema.txt) 72 | 73 | ## License 74 | 75 | [The MIT License](http://opensource.org/licenses/MIT) 76 | 77 | [![githalytics.com alpha](https://cruel-carlota.pagodabox.com/e08b202aefce41667b99d181284b1f6e "githalytics.com")](http://githalytics.com/RedisLabs/redis-quartz) -------------------------------------------------------------------------------- /schema/schema.txt: -------------------------------------------------------------------------------- 1 | JOBS 2 | ===== 3 | 4 | 1. Job 5 | type: Hash 6 | key: 'job::' 7 | 8 | Field Value 9 | job_class_name the implemented job class 10 | description job's description 11 | blocked_by blocked by instance id 12 | block_time blocked timestamp 13 | 14 | 15 | 2. JobDataMap 16 | type: Hash 17 | key: 'job_data_map::' 18 | 19 | Field Value 20 | field1 String 21 | field2 String 22 | ... 23 | 24 | 25 | 3. JobGroup 26 | type: Set 27 | key: 'job_group:' 28 | 29 | Members 30 | Job 'job::' 31 | 32 | 33 | 4. Jobs 34 | type: Set 35 | key: 'jobs' 36 | 37 | Members 38 | Job 'job::' 39 | 40 | 41 | 5. JobGroups 42 | type: Set 43 | key: 'job_groups' 44 | 45 | Members 46 | JobGroup 'job_group:' 47 | 48 | 49 | 6. JobTriggers 50 | type: Set 51 | Key: 'job_triggers:job::' 52 | 53 | Members 54 | Trigger 'trigger::' 55 | 56 | 57 | 7. PausedJobGroups 58 | type: Set 59 | Key: 'paused_job_groups' 60 | 61 | Members 62 | JobGroup 'job_group:' 63 | 64 | 65 | 8. BlockedJobs 66 | type: Set 67 | Key: 'blocked_jobs' 68 | 69 | Members 70 | Job 'job::' 71 | 72 | TRIGGERS 73 | ========= 74 | 75 | 1. Trigger 76 | type: Hash 77 | key: 'trigger::' 78 | 79 | Field Value 80 | job_hash_key the job's key 81 | next_fire_time 82 | prev_fire_time 83 | priority 84 | trigger_type 85 | calendar_name the calendar's name 86 | start_time 87 | end_time 88 | final_fire_time 89 | fire_instance_id the firing instance id 90 | misfire_instruction 91 | locked_by locking instance id 92 | lock_time locking timestamp 93 | Simple Trigger Fields 94 | repeat_count 95 | repeat_interval 96 | times_triggered 97 | Cron Trigger Fields 98 | cron_expression 99 | time_zone_id 100 | 101 | 102 | 2. TriggerGroup 103 | type: Set 104 | Key: 'trigger_group:' 105 | 106 | Members 107 | Trigger 'trigger::' 108 | 109 | 110 | 3. Triggers 111 | Set 112 | Key: 'triggers' 113 | 114 | Members 115 | Trigger 'trigger::' 116 | 117 | 118 | 3. TriggerGroups 119 | type: Set 120 | Key: 'trigger_groups' 121 | 122 | Members 123 | TriggerGroup 'trigger_group:' 124 | 125 | 126 | 4. PausedTriggerGroups 127 | type: Set 128 | Key: 'paused_trigger_groups' 129 | 130 | Members 131 | TriggerGroup 'trigger_group:' 132 | 133 | TRIGGER STATES 134 | =============== 135 | 136 | 1. Waiting 137 | type: Zset 138 | Key: 'waiting_triggers' 139 | 140 | Members Score 141 | Trigger 'trigger::' trigger's next fire time timestamp 142 | 143 | 144 | 2. Paused 145 | type: Zset 146 | Key: 'paused_triggers' 147 | 148 | Members Score 149 | Trigger 'trigger::' trigger's next fire time timestamp 150 | 151 | 152 | 3. Blocked 153 | type: Zset 154 | Key: 'blocked_triggers' 155 | 156 | Members Score 157 | Trigger 'trigger::' trigger's next fire time timestamp 158 | 159 | 160 | 4. Acquired 161 | type: Zset 162 | Key: 'acquired_triggers' 163 | 164 | Members Score 165 | Trigger 'trigger::' trigger's next fire time timestamp 166 | 167 | 168 | 5. Error 169 | type: Zset 170 | Key: 'error_triggers' 171 | 172 | Members Score 173 | Trigger 'trigger::' trigger's next fire time timestamp 174 | 175 | 176 | 6. Completed 177 | type: Zset 178 | Key: 'completed_triggers' 179 | 180 | Members Score 181 | Trigger 'trigger::' trigger's next fire time timestamp 182 | 183 | CALENDARS 184 | ========== 185 | 186 | 1. Calendar 187 | type: Hash 188 | key: 'calendar:' 189 | 190 | Field Value 191 | calendar_class the calendar's class 192 | calendar_serialized json serialized calendar 193 | 194 | 195 | 2. Calendars 196 | type: Set 197 | Key: 'calendars' 198 | 199 | Members 200 | Calendar 'calendar:' 201 | 202 | 203 | 3. CalendarTriggers 204 | type: Set 205 | Key: 'calendar_triggers:' 206 | 207 | Members 208 | Trigger 'trigger::' -------------------------------------------------------------------------------- /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 results = p.syncAndReturnAll(); 709 | 710 | for (Object result : results) { 711 | removed = (Long)result; 712 | if (removed == 1) { 713 | jedis.hset(triggerHashKey, LOCKED_BY, ""); 714 | jedis.hset(triggerHashKey, LOCK_TIME, ""); 715 | break; 716 | } 717 | } 718 | } catch (Exception ex) { 719 | removed = -1L; 720 | log.error("could not unset state for trigger: " + triggerHashKey); 721 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 722 | } 723 | return removed; 724 | } 725 | 726 | @Override 727 | public boolean removeTriggers(List triggerKeys) 728 | throws JobPersistenceException { 729 | boolean removed = triggerKeys.size() > 0; 730 | for (TriggerKey triggerKey : triggerKeys) 731 | removed = removeTrigger(triggerKey) && removed; 732 | 733 | return removed; 734 | } 735 | 736 | @Override 737 | public boolean replaceTrigger(TriggerKey triggerKey, 738 | OperableTrigger newTrigger) throws JobPersistenceException { 739 | boolean replaced = false; 740 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName()); 741 | try (Jedis jedis = pool.getResource()) { 742 | lockPool.acquire(); 743 | 744 | String jobHashKey =jedis.hget(triggerHashKey, JOB_HASH_KEY); 745 | if (jobHashKey == null || jobHashKey.isEmpty()) 746 | throw new JobPersistenceException("trigger does not exist or no job is associated with the trigger"); 747 | 748 | if (!jobHashKey.equals(createJobHashKey(newTrigger.getJobKey().getGroup(), newTrigger.getJobKey().getName()))) 749 | throw new JobPersistenceException("the new trigger is associated with a diffrent job than the existing trigger"); 750 | 751 | removeTrigger(triggerKey, jedis); 752 | storeTrigger(newTrigger, false, jedis); 753 | replaced = true; 754 | } catch (Exception ex) { 755 | log.error("could not replace trigger: " + triggerHashKey, ex); 756 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 757 | } finally { 758 | lockPool.release(); 759 | } 760 | return replaced; 761 | } 762 | 763 | @Override 764 | public OperableTrigger retrieveTrigger(TriggerKey triggerKey) throws JobPersistenceException { 765 | OperableTrigger trigger = null; 766 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName()); 767 | try (Jedis jedis = pool.getResource()) { 768 | lockPool.acquire(); 769 | trigger = retrieveTrigger(triggerKey, jedis); 770 | } catch (Exception ex) { 771 | log.error("could not retrieve trigger: " + triggerHashKey, ex); 772 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 773 | } finally { 774 | lockPool.release(); 775 | } 776 | return trigger; 777 | } 778 | 779 | /** 780 | * Retrieves trigger from redis. 781 | * 782 | * @param triggerKey the trigger key 783 | * @param jedis thread-safe redis connection 784 | * @return the operable trigger 785 | * @throws JobPersistenceException 786 | */ 787 | private OperableTrigger retrieveTrigger(TriggerKey triggerKey, Jedis jedis) throws JobPersistenceException { 788 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName()); 789 | Map trigger = jedis.hgetAll(triggerHashKey); 790 | if (!jedis.exists(triggerHashKey)) { 791 | log.debug("trigger does not exist for key: " + triggerHashKey); 792 | return null; 793 | } 794 | 795 | if (!jedis.exists(trigger.get(JOB_HASH_KEY))) { 796 | log.warn("job: " + trigger.get(JOB_HASH_KEY) + " does not exist for trigger: " + triggerHashKey); 797 | return null; 798 | } 799 | return toOperableTrigger(triggerKey, trigger); 800 | } 801 | 802 | private OperableTrigger toOperableTrigger(TriggerKey triggerKey, Map trigger) { 803 | if (TRIGGER_TYPE_SIMPLE.equals(trigger.get(TRIGGER_TYPE))) { 804 | SimpleTriggerImpl simpleTrigger = new SimpleTriggerImpl(); 805 | setOperableTriggerFields(triggerKey, trigger, simpleTrigger); 806 | if (trigger.get(REPEAT_COUNT) != null && !trigger.get(REPEAT_COUNT).isEmpty()) 807 | simpleTrigger.setRepeatCount(Integer.parseInt(trigger.get(REPEAT_COUNT))); 808 | if (trigger.get(REPEAT_INTERVAL) != null && !trigger.get(REPEAT_INTERVAL).isEmpty()) 809 | simpleTrigger.setRepeatInterval(Long.parseLong(trigger.get(REPEAT_INTERVAL))); 810 | if (trigger.get(TIMES_TRIGGERED) != null && !trigger.get(TIMES_TRIGGERED).isEmpty()) 811 | simpleTrigger.setTimesTriggered(Integer.parseInt(trigger.get(TIMES_TRIGGERED))); 812 | 813 | return simpleTrigger; 814 | } else if (TRIGGER_TYPE_CRON.equals(trigger.get(TRIGGER_TYPE))) { 815 | CronTriggerImpl cronTrigger = new CronTriggerImpl(); 816 | setOperableTriggerFields(triggerKey, trigger, cronTrigger); 817 | if (trigger.get(TIME_ZONE_ID) != null && !trigger.get(TIME_ZONE_ID).isEmpty()) 818 | cronTrigger.getTimeZone().setID(trigger.get(TIME_ZONE_ID).isEmpty() ? null : trigger.get(TIME_ZONE_ID)); 819 | try { 820 | if (trigger.get(CRON_EXPRESSION) != null && !trigger.get(CRON_EXPRESSION).isEmpty()) 821 | cronTrigger.setCronExpression(trigger.get(CRON_EXPRESSION).isEmpty() ? null : trigger.get(CRON_EXPRESSION)); 822 | } catch (ParseException ex) { 823 | log.warn("could not parse cron_expression: " + trigger.get(CRON_EXPRESSION) + " for trigger: " + createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName())); 824 | } 825 | 826 | return cronTrigger; 827 | } else { // other trigger types are not supported 828 | throw new UnsupportedOperationException(); 829 | } 830 | } 831 | 832 | private void setOperableTriggerFields(TriggerKey triggerKey, Map trigger, OperableTrigger operableTrigger) { 833 | operableTrigger.setKey(triggerKey); 834 | operableTrigger.setJobKey(new JobKey(trigger.get(JOB_HASH_KEY).split(":")[2], trigger.get(JOB_HASH_KEY).split(":")[1])); 835 | operableTrigger.setDescription(trigger.get(DESCRIPTION).isEmpty() ? null : trigger.get(DESCRIPTION)); 836 | operableTrigger.setFireInstanceId(trigger.get(FIRE_INSTANCE_ID).isEmpty() ? null : trigger.get(FIRE_INSTANCE_ID)); 837 | operableTrigger.setCalendarName(trigger.get(CALENDAR_NAME).isEmpty() ? null : trigger.get(CALENDAR_NAME)); 838 | operableTrigger.setPriority(Integer.parseInt(trigger.get(PRIORITY))); 839 | operableTrigger.setMisfireInstruction(Integer.parseInt(trigger.get(MISFIRE_INSTRUCTION))); 840 | operableTrigger.setStartTime(trigger.get(START_TIME).isEmpty() ? null : new Date(Long.parseLong(trigger.get(START_TIME)))); 841 | operableTrigger.setEndTime(trigger.get(END_TIME).isEmpty() ? null : new Date(Long.parseLong(trigger.get(END_TIME)))); 842 | operableTrigger.setNextFireTime(trigger.get(NEXT_FIRE_TIME).isEmpty() ? null : new Date(Long.parseLong(trigger.get(NEXT_FIRE_TIME)))); 843 | operableTrigger.setPreviousFireTime(trigger.get(PREV_FIRE_TIME).isEmpty() ? null : new Date(Long.parseLong(trigger.get(PREV_FIRE_TIME)))); 844 | } 845 | 846 | @Override 847 | public boolean checkExists(JobKey jobKey) throws JobPersistenceException { 848 | String jobHashKey = createJobHashKey(jobKey.getGroup(), jobKey.getName()); 849 | try (Jedis jedis = pool.getResource()) { 850 | return jedis.exists(jobHashKey); 851 | } catch (Exception ex) { 852 | log.error("could not check if job: " + jobHashKey + " exists", ex); 853 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 854 | } 855 | } 856 | 857 | @Override 858 | public boolean checkExists(TriggerKey triggerKey) 859 | throws JobPersistenceException { 860 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName()); 861 | try (Jedis jedis = pool.getResource()) { 862 | return jedis.exists(triggerHashKey); 863 | } catch (Exception ex) { 864 | log.error("could not check if trigger: " + triggerHashKey + " exists", ex); 865 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 866 | } 867 | } 868 | 869 | @Override 870 | public void clearAllSchedulingData() throws JobPersistenceException { 871 | 872 | try (Jedis jedis = pool.getResource()) { 873 | lockPool.acquire(); 874 | 875 | // removing all jobs 876 | Set jobs = jedis.smembers(JOBS_SET); 877 | for (String job : jobs) 878 | removeJob(new JobKey(job.split(":")[2], job.split(":")[1]), jedis); 879 | 880 | // removing all triggers 881 | Set triggers = jedis.smembers(TRIGGERS_SET); 882 | for (String trigger : triggers) 883 | removeTrigger(new TriggerKey(trigger.split(":")[2], trigger.split(":")[1]), jedis); 884 | 885 | // removing all calendars 886 | Set calendars = jedis.smembers(CALENDARS_SET); 887 | for (String calendar : calendars) 888 | removeCalendar(calendar.split(":")[1], jedis); 889 | 890 | log.debug("all scheduling data cleared!"); 891 | } catch (Exception ex) { 892 | log.error("could not remove scheduling data", ex); 893 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 894 | } finally { 895 | lockPool.release(); 896 | } 897 | } 898 | 899 | @Override 900 | public void storeCalendar(String name, Calendar calendar, 901 | boolean replaceExisting, boolean updateTriggers) 902 | throws ObjectAlreadyExistsException, JobPersistenceException { 903 | 904 | String calendarHashKey = createCalendarHashKey(name); 905 | try (Jedis jedis = pool.getResource()) { 906 | lockPool.acquire(); 907 | storeCalendar(name, calendar, replaceExisting, updateTriggers, jedis); 908 | } catch (ObjectAlreadyExistsException ex) { 909 | log.warn(calendarHashKey + " already exists"); 910 | throw ex; 911 | } catch (Exception ex) { 912 | log.error("could not store calendar: " + calendarHashKey, ex); 913 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 914 | } finally { 915 | lockPool.release(); 916 | } 917 | } 918 | 919 | /** 920 | * Store calendar in redis. 921 | * 922 | * @param name the name 923 | * @param calendar the calendar 924 | * @param replaceExisting the replace existing 925 | * @param updateTriggers the update triggers 926 | * @param jedis thread-safe redis connection 927 | * @throws ObjectAlreadyExistsException the object already exists exception 928 | * @throws JobPersistenceException 929 | */ 930 | private void storeCalendar(String name, Calendar calendar, 931 | boolean replaceExisting, boolean updateTriggers, Jedis jedis) 932 | throws ObjectAlreadyExistsException, JobPersistenceException { 933 | 934 | String calendarHashKey = createCalendarHashKey(name); 935 | if (jedis.exists(calendarHashKey) && !replaceExisting) 936 | throw new ObjectAlreadyExistsException(calendarHashKey + " already exists"); 937 | 938 | Gson gson = new Gson(); 939 | Map calendarHash = new HashMap<>(); 940 | calendarHash.put(CALENDAR_CLASS, calendar.getClass().getName()); 941 | calendarHash.put(CALENDAR_SERIALIZED, gson.toJson(calendar)); 942 | 943 | jedis.hmset(calendarHashKey, calendarHash); 944 | jedis.sadd(CALENDARS_SET, calendarHashKey); 945 | 946 | if (updateTriggers) { 947 | String calendarTriggersSetkey = createCalendarTriggersSetKey(name); 948 | Set triggerHasjKeys = jedis.smembers(calendarTriggersSetkey); 949 | for (String triggerHashKey : triggerHasjKeys) { 950 | OperableTrigger trigger = retrieveTrigger(new TriggerKey(triggerHashKey.split(":")[2], triggerHashKey.split(":")[1]), jedis); 951 | long removed = jedis.zrem(RedisTriggerState.WAITING.getKey(), triggerHashKey); 952 | trigger.updateWithNewCalendar(calendar, getMisfireThreshold()); 953 | if (removed == 1) 954 | setTriggerState(RedisTriggerState.WAITING, (double)trigger.getNextFireTime().getTime(), triggerHashKey); 955 | } 956 | } 957 | } 958 | 959 | @Override 960 | public boolean removeCalendar(String calName) 961 | throws JobPersistenceException { 962 | boolean removed = false; 963 | String calendarHashKey = createCalendarHashKey(calName); 964 | try (Jedis jedis = pool.getResource()) { 965 | lockPool.acquire(); 966 | if (jedis.exists(calendarHashKey)) { 967 | removeCalendar(calName, jedis); 968 | removed = true; 969 | } 970 | } catch (Exception ex) { 971 | log.error("could not remove calendar: " + calendarHashKey, ex); 972 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 973 | } finally { 974 | lockPool.release(); 975 | } 976 | return removed; 977 | } 978 | 979 | /** 980 | * Removes the calendar from redis. 981 | * 982 | * @param calName the calendar name 983 | * @param jedis thread-safe redis connection 984 | * @throws JobPersistenceException 985 | */ 986 | private void removeCalendar(String calName, Jedis jedis) 987 | throws JobPersistenceException { 988 | String calendarHashKey = createCalendarHashKey(calName); 989 | 990 | // checking if there are triggers pointing to this calendar 991 | String calendarTriggersSetkey = createCalendarTriggersSetKey(calName); 992 | if (jedis.scard(calendarTriggersSetkey) > 0) 993 | throw new JobPersistenceException("there are triggers pointing to: " + calendarHashKey + ", calendar can't be removed"); 994 | 995 | // removing the calendar 996 | jedis.del(calendarHashKey); 997 | jedis.srem(CALENDARS_SET, calendarHashKey); 998 | } 999 | 1000 | @Override 1001 | public Calendar retrieveCalendar(String calName) 1002 | throws JobPersistenceException { 1003 | Calendar calendar = null; 1004 | String calendarHashKey = createCalendarHashKey(calName); 1005 | try (Jedis jedis = pool.getResource()) { 1006 | lockPool.acquire(); 1007 | calendar = retrieveCalendar(calName, jedis); 1008 | } catch (Exception ex) { 1009 | log.error("could not retrieve calendar: " + calendarHashKey, ex); 1010 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1011 | } finally { 1012 | lockPool.release(); 1013 | } 1014 | return calendar; 1015 | } 1016 | 1017 | /** 1018 | * Retrieve calendar. 1019 | * 1020 | * @param calname the calendar name 1021 | * @param jedis thread-safe redis connection 1022 | * @return the calendar 1023 | * @throws JobPersistenceException 1024 | */ 1025 | @SuppressWarnings({ "rawtypes", "unchecked" }) 1026 | private Calendar retrieveCalendar(String calName, Jedis jedis) 1027 | throws JobPersistenceException { 1028 | Calendar calendar = null; 1029 | try { 1030 | String calendarHashKey = createCalendarHashKey(calName); 1031 | if (!jedis.exists(calendarHashKey)) 1032 | throw new JobPersistenceException(calendarHashKey + " does not exist"); 1033 | 1034 | Gson gson = new Gson(); 1035 | Class calClass = Class.forName(jedis.hget(calendarHashKey, CALENDAR_CLASS)); 1036 | calendar = (Calendar) gson.fromJson(jedis.hget(calendarHashKey, CALENDAR_SERIALIZED), calClass); 1037 | } catch (ClassNotFoundException ex) { 1038 | log.warn("class not found for calendar: " + calName, ex); 1039 | } 1040 | return calendar; 1041 | } 1042 | 1043 | @Override 1044 | public int getNumberOfJobs() throws JobPersistenceException { 1045 | try (Jedis jedis = pool.getResource()) { 1046 | lockPool.acquire(); 1047 | return jedis.scard(JOBS_SET).intValue(); 1048 | } catch (Exception ex) { 1049 | log.error("could not get number of jobs", ex); 1050 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1051 | } finally { 1052 | lockPool.release(); 1053 | } 1054 | } 1055 | 1056 | @Override 1057 | public int getNumberOfTriggers() throws JobPersistenceException { 1058 | try (Jedis jedis = pool.getResource()) { 1059 | lockPool.acquire(); 1060 | return jedis.scard(TRIGGERS_SET).intValue(); 1061 | } catch (Exception ex) { 1062 | log.error("could not get number of triggers", ex); 1063 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1064 | } finally { 1065 | lockPool.release(); 1066 | } 1067 | } 1068 | 1069 | @Override 1070 | public int getNumberOfCalendars() throws JobPersistenceException { 1071 | try (Jedis jedis = pool.getResource()) { 1072 | lockPool.acquire(); 1073 | return jedis.scard(CALENDARS_SET).intValue(); 1074 | } catch (Exception ex) { 1075 | log.error("could not get number of triggers", ex); 1076 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1077 | } finally { 1078 | lockPool.release(); 1079 | } 1080 | } 1081 | 1082 | @Override 1083 | public Set getJobKeys(GroupMatcher matcher) 1084 | throws JobPersistenceException { 1085 | if (matcher.getCompareWithOperator() != StringOperatorName.EQUALS) 1086 | throw new UnsupportedOperationException(); 1087 | 1088 | Set jobKeys = new HashSet<>(); 1089 | String jobGroupSetKey = createJobGroupSetKey(matcher.getCompareToValue()); 1090 | try (Jedis jedis = pool.getResource()) { 1091 | lockPool.acquire(); 1092 | if(jedis.sismember(JOB_GROUPS_SET, jobGroupSetKey)) { 1093 | Set jobs = jedis.smembers(jobGroupSetKey); 1094 | for(String job : jobs) 1095 | jobKeys.add(new JobKey(job.split(":")[2], job.split(":")[1])); 1096 | } 1097 | } catch (Exception ex) { 1098 | log.error("could not get job keys for job group: " + jobGroupSetKey, ex); 1099 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1100 | } finally { 1101 | lockPool.release(); 1102 | } 1103 | 1104 | return jobKeys; 1105 | } 1106 | 1107 | @Override 1108 | public Set getTriggerKeys(GroupMatcher matcher) 1109 | throws JobPersistenceException { 1110 | if (matcher.getCompareWithOperator() != StringOperatorName.EQUALS) 1111 | throw new UnsupportedOperationException(); 1112 | 1113 | Set triggerKeys = new HashSet<>(); 1114 | String triggerGroupSetKey = createTriggerGroupSetKey(matcher.getCompareToValue()); 1115 | try (Jedis jedis = pool.getResource()) { 1116 | lockPool.acquire(); 1117 | if(jedis.sismember(TRIGGER_GROUPS_SET, triggerGroupSetKey)) { 1118 | Set triggers = jedis.smembers(triggerGroupSetKey); 1119 | for(String trigger : triggers) 1120 | triggerKeys.add(new TriggerKey(trigger.split(":")[2], trigger.split(":")[1])); 1121 | } 1122 | } catch (Exception ex) { 1123 | log.error("could not get trigger keys for trigger group: " + triggerGroupSetKey, ex); 1124 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1125 | } finally { 1126 | lockPool.release(); 1127 | } 1128 | 1129 | return triggerKeys; 1130 | } 1131 | 1132 | @Override 1133 | public List getJobGroupNames() throws JobPersistenceException { 1134 | try (Jedis jedis = pool.getResource()) { 1135 | lockPool.acquire(); 1136 | return new ArrayList<>(jedis.smembers(JOB_GROUPS_SET)); 1137 | } catch(Exception ex) { 1138 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1139 | } finally { 1140 | lockPool.release(); 1141 | } 1142 | } 1143 | 1144 | @Override 1145 | public List getTriggerGroupNames() throws JobPersistenceException { 1146 | try (Jedis jedis = pool.getResource()) { 1147 | lockPool.acquire(); 1148 | return new ArrayList<>(jedis.smembers(TRIGGER_GROUPS_SET)); 1149 | } catch(Exception ex) { 1150 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1151 | } finally { 1152 | lockPool.release(); 1153 | } 1154 | } 1155 | 1156 | @Override 1157 | public List getCalendarNames() throws JobPersistenceException { 1158 | try (Jedis jedis = pool.getResource()) { 1159 | lockPool.acquire(); 1160 | return new ArrayList<>(jedis.smembers(CALENDARS_SET)); 1161 | } catch(Exception ex) { 1162 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1163 | } finally { 1164 | lockPool.release(); 1165 | } 1166 | } 1167 | 1168 | @Override 1169 | public List getTriggersForJob(JobKey jobKey) 1170 | throws JobPersistenceException { 1171 | String jobTriggerSetkey = createJobTriggersSetKey(jobKey.getGroup(), jobKey.getName()); 1172 | List triggers = new ArrayList<>(); 1173 | try (Jedis jedis = pool.getResource()) { 1174 | lockPool.acquire(); 1175 | triggers = getTriggersForJob(jobTriggerSetkey, jedis); 1176 | } catch (Exception ex) { 1177 | log.error("could not get triggers for job_triggers: " + jobTriggerSetkey, ex); 1178 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1179 | } finally { 1180 | lockPool.release(); 1181 | } 1182 | return triggers; 1183 | } 1184 | 1185 | /** 1186 | * Gets triggers for a job. 1187 | * 1188 | * @param jobTriggerHashkey the job_trigger hash key 1189 | * @param jedis thread-safe redis connection 1190 | * @return the triggers for the job 1191 | * @throws JobPersistenceException 1192 | */ 1193 | private List getTriggersForJob(String jobTriggerHashkey, Jedis jedis) throws JobPersistenceException { 1194 | List triggers = new ArrayList<>(); 1195 | Set triggerHashkeys = jedis.smembers(jobTriggerHashkey); 1196 | for (String triggerHashkey : triggerHashkeys) 1197 | triggers.add(retrieveTrigger(new TriggerKey(triggerHashkey.split(":")[2], triggerHashkey.split(":")[1]), jedis)); 1198 | 1199 | return triggers; 1200 | } 1201 | 1202 | @Override 1203 | public TriggerState getTriggerState(TriggerKey triggerKey) 1204 | throws JobPersistenceException { 1205 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName()); 1206 | try (Jedis jedis = pool.getResource()) { 1207 | lockPool.acquire(); 1208 | 1209 | if (jedis.zscore(RedisTriggerState.PAUSED.getKey(), triggerHashKey) != null || jedis.zscore(RedisTriggerState.PAUSED_BLOCKED.getKey(), triggerHashKey)!= null) 1210 | return TriggerState.PAUSED; 1211 | else if (jedis.zscore(RedisTriggerState.BLOCKED.getKey(), triggerHashKey) != null) 1212 | return TriggerState.BLOCKED; 1213 | else if (jedis.zscore(RedisTriggerState.WAITING.getKey(), triggerHashKey) != null || jedis.zscore(RedisTriggerState.ACQUIRED.getKey(), triggerHashKey) != null) 1214 | return TriggerState.NORMAL; 1215 | else if (jedis.zscore(RedisTriggerState.COMPLETED.getKey(), triggerHashKey) != null) 1216 | return TriggerState.COMPLETE; 1217 | else if (jedis.zscore(RedisTriggerState.ERROR.getKey(), triggerHashKey) != null) 1218 | return TriggerState.ERROR; 1219 | else 1220 | return TriggerState.NONE; 1221 | } catch (Exception ex) { 1222 | log.error("could not get trigger state: " + triggerHashKey, ex); 1223 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1224 | } finally { 1225 | lockPool.release(); 1226 | } 1227 | } 1228 | 1229 | @Override 1230 | public void pauseTrigger(TriggerKey triggerKey) 1231 | throws JobPersistenceException { 1232 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName()); 1233 | try (Jedis jedis = pool.getResource()) { 1234 | lockPool.acquire(); 1235 | pauseTrigger(triggerKey, jedis); 1236 | } catch (Exception ex) { 1237 | log.error("could not pause trigger: " + triggerHashKey, ex); 1238 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1239 | } finally { 1240 | lockPool.release(); 1241 | } 1242 | } 1243 | 1244 | /** 1245 | * Pause trigger in redis. 1246 | * 1247 | * @param triggerKey the trigger key 1248 | * @param jedis thread-safe redis connection 1249 | * @throws JobPersistenceException 1250 | */ 1251 | private void pauseTrigger(TriggerKey triggerKey, Jedis jedis) throws JobPersistenceException { 1252 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName()); 1253 | if (!jedis.exists(triggerHashKey)) 1254 | throw new JobPersistenceException("trigger: " + triggerHashKey + " does not exist"); 1255 | 1256 | if (jedis.zscore(RedisTriggerState.COMPLETED.getKey(), triggerHashKey) != null) 1257 | return; 1258 | 1259 | Long nextFireTime = jedis.hget(triggerHashKey, NEXT_FIRE_TIME).isEmpty() ? -1 : Long.parseLong(jedis.hget(triggerHashKey, NEXT_FIRE_TIME)); 1260 | if (jedis.zscore(RedisTriggerState.BLOCKED.getKey(), triggerHashKey) != null) 1261 | setTriggerState(RedisTriggerState.PAUSED_BLOCKED, (double)nextFireTime, triggerHashKey); 1262 | else 1263 | setTriggerState(RedisTriggerState.PAUSED, (double)nextFireTime, triggerHashKey); 1264 | } 1265 | 1266 | @Override 1267 | public Collection pauseTriggers(GroupMatcher matcher) 1268 | throws JobPersistenceException { 1269 | if (matcher.getCompareWithOperator() != StringOperatorName.EQUALS) 1270 | throw new UnsupportedOperationException(); 1271 | 1272 | Set pausedTriggerdGroups = new HashSet<>(); 1273 | String triggerGroupSetKey = createTriggerGroupSetKey(matcher.getCompareToValue()); 1274 | try (Jedis jedis = pool.getResource()) { 1275 | lockPool.acquire(); 1276 | if (pauseTriggers(triggerGroupSetKey, jedis)) 1277 | pausedTriggerdGroups.add(triggerGroupSetKey); // as we currently support only EQUALS matcher's operator, the paused group set will consist of one paused group only. 1278 | } catch (Exception ex) { 1279 | log.error("could not pause triggers group: " + triggerGroupSetKey, ex); 1280 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1281 | } finally { 1282 | lockPool.release(); 1283 | } 1284 | return pausedTriggerdGroups; 1285 | } 1286 | 1287 | /** 1288 | * Pause triggers. 1289 | * 1290 | * @param triggerGroupHashKey the trigger group hash key 1291 | * @param jedis thread-safe redis connection 1292 | * @throws JobPersistenceException 1293 | */ 1294 | private boolean pauseTriggers(String triggerGroupHashKey, Jedis jedis) throws JobPersistenceException { 1295 | if (jedis.sadd(PAUSED_TRIGGER_GROUPS_SET, triggerGroupHashKey) > 0) { 1296 | Set triggers = jedis.smembers(triggerGroupHashKey); 1297 | for(String trigger : triggers) 1298 | pauseTrigger(new TriggerKey(trigger.split(":")[2], trigger.split(":")[1]), jedis); 1299 | return true; 1300 | } 1301 | return false; 1302 | } 1303 | 1304 | @Override 1305 | public void pauseJob(JobKey jobKey) throws JobPersistenceException { 1306 | String jobHashKey = createJobHashKey(jobKey.getGroup(),jobKey.getName()); 1307 | try (Jedis jedis = pool.getResource()) { 1308 | lockPool.acquire(); 1309 | pauseJob(jobHashKey, jedis); 1310 | } catch (Exception ex) { 1311 | log.error("could not pause job: " + jobHashKey, ex); 1312 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1313 | } finally { 1314 | lockPool.release(); 1315 | } 1316 | } 1317 | 1318 | /** 1319 | * Pause job. 1320 | * 1321 | * @param jobKey the job key 1322 | * @param jedis thread-safe redis connection 1323 | * @throws JobPersistenceException 1324 | */ 1325 | private void pauseJob(String jobHashKey, Jedis jedis) throws JobPersistenceException { 1326 | if (!jedis.sismember(JOBS_SET, jobHashKey)) 1327 | throw new JobPersistenceException("job: " + jobHashKey + " des not exist"); 1328 | 1329 | String jobTriggerSetkey = createJobTriggersSetKey(jobHashKey.split(":")[1], jobHashKey.split(":")[2]); 1330 | List triggers = getTriggersForJob(jobTriggerSetkey, jedis); 1331 | for (OperableTrigger trigger : triggers) 1332 | pauseTrigger(trigger.getKey(), jedis); 1333 | } 1334 | 1335 | @Override 1336 | public Collection pauseJobs(GroupMatcher groupMatcher) 1337 | throws JobPersistenceException { 1338 | if (groupMatcher.getCompareWithOperator() != StringOperatorName.EQUALS) 1339 | throw new UnsupportedOperationException(); 1340 | 1341 | Set pausedJobGroups = new HashSet<>(); 1342 | String jobGroupSetKey = createJobGroupSetKey(groupMatcher.getCompareToValue()); 1343 | try (Jedis jedis = pool.getResource()) { 1344 | lockPool.acquire(); 1345 | 1346 | if (jedis.sadd(PAUSED_JOB_GROUPS_SET, jobGroupSetKey) > 0) { 1347 | pausedJobGroups.add(jobGroupSetKey); 1348 | Set jobs = jedis.smembers(jobGroupSetKey); 1349 | for (String job : jobs) 1350 | pauseJob(job, jedis); 1351 | } 1352 | } catch (Exception ex) { 1353 | log.error("could not pause job group: " + jobGroupSetKey, ex); 1354 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1355 | } finally { 1356 | lockPool.release(); 1357 | } 1358 | return pausedJobGroups; 1359 | } 1360 | 1361 | @Override 1362 | public void resumeTrigger(TriggerKey triggerKey) 1363 | throws JobPersistenceException { 1364 | String triggerHashKey = createTriggerHashKey(triggerKey.getGroup(), triggerKey.getName()); 1365 | try (Jedis jedis = pool.getResource()) { 1366 | lockPool.acquire(); 1367 | 1368 | OperableTrigger trigger = retrieveTrigger(new TriggerKey(triggerHashKey.split(":")[2], triggerHashKey.split(":")[1]), jedis); 1369 | resumeTrigger(trigger, jedis); 1370 | if (trigger != null) 1371 | applyMisfire(trigger, jedis); 1372 | } catch (Exception ex) { 1373 | log.error("could not resume trigger: " + triggerHashKey, ex); 1374 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1375 | } finally { 1376 | lockPool.release(); 1377 | } 1378 | } 1379 | 1380 | /** 1381 | * Resume a trigger in redis. 1382 | * 1383 | * @param trigger the trigger 1384 | * @param jedis thread-safe redis connection 1385 | * @throws JobPersistenceException 1386 | */ 1387 | private void resumeTrigger(OperableTrigger trigger, Jedis jedis) throws JobPersistenceException { 1388 | String triggerHashKey = createTriggerHashKey(trigger.getKey().getGroup(), trigger.getKey().getName()); 1389 | if (!jedis.sismember(TRIGGERS_SET, triggerHashKey)) 1390 | throw new JobPersistenceException("trigger: " + trigger + " does not exist"); 1391 | 1392 | if (jedis.zscore(RedisTriggerState.PAUSED.getKey(), triggerHashKey) == null && jedis.zscore(RedisTriggerState.PAUSED_BLOCKED.getKey(), triggerHashKey) == null) 1393 | throw new JobPersistenceException("trigger: " + trigger + " is not paused"); 1394 | 1395 | String jobHashKey = createJobHashKey(trigger.getJobKey().getGroup(), trigger.getJobKey().getName()); 1396 | Date nextFireTime = trigger.getNextFireTime(); 1397 | if (nextFireTime != null) { 1398 | if (jedis.sismember(BLOCKED_JOBS_SET, jobHashKey)) 1399 | setTriggerState(RedisTriggerState.BLOCKED, (double)nextFireTime.getTime(), triggerHashKey); 1400 | else 1401 | setTriggerState(RedisTriggerState.WAITING, (double)nextFireTime.getTime(), triggerHashKey); 1402 | } 1403 | } 1404 | 1405 | @Override 1406 | public Collection resumeTriggers(GroupMatcher matcher) 1407 | throws JobPersistenceException { 1408 | Set resumedTriggerdGroups; 1409 | String triggerGroupSetKey = createTriggerGroupSetKey(matcher.getCompareToValue()); 1410 | try (Jedis jedis = pool.getResource()) { 1411 | lockPool.acquire(); 1412 | resumedTriggerdGroups = resumeTriggers(triggerGroupSetKey, jedis); 1413 | } catch (Exception ex) { 1414 | log.error("could not pause triggers group: " + triggerGroupSetKey, ex); 1415 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1416 | } finally { 1417 | lockPool.release(); 1418 | } 1419 | return resumedTriggerdGroups; 1420 | } 1421 | 1422 | /** 1423 | * Resume triggers in redis. 1424 | * 1425 | * @param triggerGroupHashKey the trigger group hash key 1426 | * @param jedis thread-safe redis connection 1427 | * @return resumed trigger groups set 1428 | * @throws JobPersistenceException the job persistence exception 1429 | */ 1430 | private Set resumeTriggers(String triggerGroupHashKey, Jedis jedis) throws JobPersistenceException { 1431 | Set resumedTriggerdGroups = new HashSet<>(); 1432 | jedis.srem(PAUSED_TRIGGER_GROUPS_SET, triggerGroupHashKey); 1433 | Set triggerHashKeys = jedis.smembers(triggerGroupHashKey); 1434 | for(String triggerHashKey : triggerHashKeys) { 1435 | OperableTrigger trigger = retrieveTrigger(new TriggerKey(triggerHashKey.split(":")[2], triggerHashKey.split(":")[1]), jedis); 1436 | resumeTrigger(trigger, jedis); 1437 | resumedTriggerdGroups.add(trigger.getKey().getGroup()); // as we currently support only EQUALS matcher's operator, the paused group set will consist of one paused group only. 1438 | } 1439 | 1440 | return resumedTriggerdGroups; 1441 | } 1442 | 1443 | @Override 1444 | public Set getPausedTriggerGroups() throws JobPersistenceException { 1445 | try (Jedis jedis = pool.getResource()) { 1446 | lockPool.acquire(); 1447 | return jedis.smembers(PAUSED_TRIGGER_GROUPS_SET); 1448 | } catch(Exception ex) { 1449 | log.error("could not get paused trigger groups", ex); 1450 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1451 | } finally { 1452 | lockPool.release(); 1453 | } 1454 | } 1455 | 1456 | @Override 1457 | public void resumeJob(JobKey jobKey) throws JobPersistenceException { 1458 | String jobHashKey = createJobHashKey(jobKey.getGroup(),jobKey.getName()); 1459 | try (Jedis jedis = pool.getResource()) { 1460 | lockPool.acquire(); 1461 | resumeJob(jobKey, jedis); 1462 | } catch (Exception ex) { 1463 | log.error("could not resume job: " + jobHashKey, ex); 1464 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1465 | } finally { 1466 | lockPool.release(); 1467 | } 1468 | } 1469 | 1470 | /** 1471 | * Resume a job in redis. 1472 | * 1473 | * @param jobKey the job key 1474 | * @param jedis thread-safe redis connection 1475 | * @throws JobPersistenceException 1476 | */ 1477 | private void resumeJob(JobKey jobKey, Jedis jedis) throws JobPersistenceException { 1478 | String jobHashKey = createJobHashKey(jobKey.getGroup(),jobKey.getName()); 1479 | if (!jedis.sismember(JOBS_SET, jobHashKey)) 1480 | throw new JobPersistenceException("job: " + jobHashKey + " des not exist"); 1481 | 1482 | List triggers = getTriggersForJob(jobKey); 1483 | for (OperableTrigger trigger : triggers) 1484 | resumeTrigger(trigger, jedis); 1485 | } 1486 | 1487 | @Override 1488 | public Collection resumeJobs(GroupMatcher matcher) 1489 | throws JobPersistenceException { 1490 | if (matcher.getCompareWithOperator() != StringOperatorName.EQUALS) 1491 | throw new UnsupportedOperationException(); 1492 | 1493 | Set resumedJobGroups = new HashSet<>(); 1494 | String jobGroupSetKey = createJobGroupSetKey(matcher.getCompareToValue()); 1495 | try (Jedis jedis = pool.getResource()) { 1496 | lockPool.acquire(); 1497 | 1498 | if(!jedis.sismember(JOB_GROUPS_SET, jobGroupSetKey)) 1499 | throw new JobPersistenceException("job group: " + jobGroupSetKey + " does not exist"); 1500 | 1501 | if (jedis.srem(PAUSED_JOB_GROUPS_SET, jobGroupSetKey) > 0) 1502 | resumedJobGroups.add(jobGroupSetKey); 1503 | 1504 | Set jobs = jedis.smembers(jobGroupSetKey); 1505 | for (String job : jobs) 1506 | resumeJob(new JobKey(job.split(":")[2], job.split(":")[1]), jedis); 1507 | } catch (Exception ex) { 1508 | log.error("could not resume job group: " + jobGroupSetKey, ex); 1509 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1510 | } finally { 1511 | lockPool.release(); 1512 | } 1513 | return resumedJobGroups; 1514 | } 1515 | 1516 | @Override 1517 | public void pauseAll() throws JobPersistenceException { 1518 | try (Jedis jedis = pool.getResource()) { 1519 | lockPool.acquire(); 1520 | List triggerGroups = new ArrayList<>(jedis.smembers(TRIGGER_GROUPS_SET)); 1521 | for (String triggerGroup : triggerGroups) 1522 | pauseTriggers(triggerGroup, jedis); 1523 | } catch(Exception ex) { 1524 | log.error("could not pause all triggers", ex); 1525 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1526 | } finally { 1527 | lockPool.release(); 1528 | } 1529 | } 1530 | 1531 | @Override 1532 | public void resumeAll() throws JobPersistenceException { 1533 | try (Jedis jedis = pool.getResource()) { 1534 | lockPool.acquire(); 1535 | List triggerGroups = new ArrayList<>(jedis.smembers(TRIGGER_GROUPS_SET)); 1536 | for (String triggerGroup : triggerGroups) 1537 | resumeTriggers(triggerGroup, jedis); 1538 | } catch(Exception ex) { 1539 | log.error("could not resume all triggers", ex); 1540 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1541 | } finally { 1542 | lockPool.release(); 1543 | } 1544 | } 1545 | 1546 | @Override 1547 | public List acquireNextTriggers(long noLaterThan, 1548 | int maxCount, long timeWindow) throws JobPersistenceException { 1549 | 1550 | // Run job store background functionality periodically in acquireNextTriggers since we know this is called periodically by the scheduler 1551 | keepAlive(); 1552 | releaseTriggersCron(); 1553 | 1554 | List acquiredTriggers = new ArrayList<>(); 1555 | try (Jedis jedis = pool.getResource()) { 1556 | lockPool.acquire(); 1557 | boolean retry = true; 1558 | while (retry) { 1559 | retry = false; 1560 | Set acquiredJobHashKeysForNoConcurrentExec = new HashSet<>(); 1561 | Set triggerTuples = jedis.zrangeByScoreWithScores(RedisTriggerState.WAITING.getKey(), 0d, (double)(noLaterThan + timeWindow), 0, maxCount); 1562 | for (Tuple triggerTuple : triggerTuples) { 1563 | OperableTrigger trigger = retrieveTrigger(new TriggerKey(triggerTuple.getElement().split(":")[2], triggerTuple.getElement().split(":")[1]), jedis); 1564 | 1565 | // handling misfires 1566 | if (applyMisfire(trigger, jedis)) { 1567 | log.debug("misfired trigger: " + triggerTuple.getElement()); 1568 | retry = true; 1569 | break; 1570 | } 1571 | 1572 | // if the trigger has no next fire time its WAITING state should be unset 1573 | if (trigger.getNextFireTime() == null) { 1574 | unsetTriggerState(triggerTuple.getElement()); 1575 | continue; 1576 | } 1577 | 1578 | // if the trigger's job is annotated as @DisallowConcurrentExecution, check if one of its triggers were already acquired 1579 | String jobHashKey = createJobHashKey(trigger.getJobKey().getGroup(), trigger.getJobKey().getName()); 1580 | if (isJobConcurrentExectionDisallowed(jedis.hget(jobHashKey, JOB_CLASS))) { 1581 | if (acquiredJobHashKeysForNoConcurrentExec.contains(jobHashKey)) // a trigger is already acquired for this job, continue to the next trigger 1582 | continue; 1583 | else 1584 | acquiredJobHashKeysForNoConcurrentExec.add(jobHashKey); 1585 | } 1586 | 1587 | // acquiring trigger 1588 | jedis.hset(triggerTuple.getElement(), LOCKED_BY, instanceId); 1589 | jedis.hset(triggerTuple.getElement(), LOCK_TIME, Long.toString(System.currentTimeMillis())); 1590 | setTriggerState(RedisTriggerState.ACQUIRED, triggerTuple.getScore(), triggerTuple.getElement()); 1591 | acquiredTriggers.add(trigger); 1592 | log.debug("trigger: " + triggerTuple.getElement() + " acquired"); 1593 | } 1594 | } 1595 | } catch(Exception ex) { 1596 | log.error("could not acquire next triggers", ex); 1597 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1598 | } finally { 1599 | lockPool.release(); 1600 | } 1601 | return acquiredTriggers; 1602 | } 1603 | 1604 | /** 1605 | * Releasing triggers of non-alive schedulers. 1606 | * A `releaseTriggersInterval` should be configured for this mechanism to run. 1607 | */ 1608 | private void releaseTriggersCron() { 1609 | if (this.releaseTriggersInterval > 0) { 1610 | long lastTriggersReleseTime = getLastTriggersReleaseTime(); 1611 | if ((System.currentTimeMillis() - lastTriggersReleseTime) > (this.releaseTriggersInterval)) { 1612 | releaseOrphanedTriggers(RedisTriggerState.ACQUIRED, RedisTriggerState.WAITING); 1613 | releaseOrphanedTriggers(RedisTriggerState.BLOCKED, RedisTriggerState.WAITING); 1614 | releaseOrphanedTriggers(RedisTriggerState.PAUSED_BLOCKED, RedisTriggerState.PAUSED); 1615 | 1616 | setLastTriggersReleaseTime(Long.toString(System.currentTimeMillis())); 1617 | } 1618 | } 1619 | } 1620 | 1621 | private long getLastTriggersReleaseTime() { 1622 | long lastTriggersReleaseTime = 0; 1623 | try (Jedis jedis = pool.getResource()) { 1624 | if (jedis.exists(LAST_TRIGGERS_RELEASE_TIME)) 1625 | lastTriggersReleaseTime = Long.parseLong(jedis.get(LAST_TRIGGERS_RELEASE_TIME)); 1626 | } catch (Exception ex) { 1627 | log.error("could not get last trigger release time"); 1628 | } 1629 | return lastTriggersReleaseTime; 1630 | } 1631 | 1632 | private void setLastTriggersReleaseTime(String time) { 1633 | try (Jedis jedis = pool.getResource()) { 1634 | jedis.set(LAST_TRIGGERS_RELEASE_TIME, time); 1635 | } catch (Exception ex) { 1636 | log.error("could not get last triggerrelease time"); 1637 | } 1638 | } 1639 | 1640 | 1641 | /** 1642 | * Release triggers from a given current state to a given new state, 1643 | * if its locking scheduler instance id is non-alive for over 10 minutes. 1644 | * 1645 | * 1646 | * @param currentState the current orphaned trigger state 1647 | * @param newState the new state of the orphaned trigger 1648 | */ 1649 | private void releaseOrphanedTriggers(RedisTriggerState currentState, RedisTriggerState newState) { 1650 | final int ALIVE_TIMEOUT = 10 * 60 * 1000; // 10 minutes non-alive scheduler timeout 1651 | Map instanceIdLastAlive = new HashMap<>(); 1652 | try (Jedis jedis = pool.getResource()) { 1653 | lockPool.acquire(); 1654 | 1655 | Set triggerTuples = jedis.zrangeWithScores(currentState.getKey(), 0, -1); 1656 | for (Tuple triggerTuple : triggerTuples) { 1657 | String lockedByInstanceId = jedis.hget(triggerTuple.getElement(), LOCKED_BY); 1658 | if (lockedByInstanceId != null && !lockedByInstanceId.isEmpty()) { 1659 | if (!instanceIdLastAlive.containsKey(lockedByInstanceId)) { 1660 | long lastAliveTime = jedis.exists(lockedByInstanceId) ? Long.parseLong(jedis.get(lockedByInstanceId)) : 0; 1661 | instanceIdLastAlive.put(lockedByInstanceId, lastAliveTime); 1662 | } 1663 | if ((System.currentTimeMillis() - instanceIdLastAlive.get(lockedByInstanceId)) > (ALIVE_TIMEOUT)) 1664 | setTriggerState(newState, triggerTuple.getScore(), triggerTuple.getElement()); 1665 | } 1666 | } 1667 | } catch (JobPersistenceException | NumberFormatException | InterruptedException ex) { 1668 | log.error("could not release orphaned triggers from: " + currentState, ex); 1669 | } finally { 1670 | lockPool.release(); 1671 | } 1672 | } 1673 | 1674 | /** 1675 | * Release triggers locked by an instance id. 1676 | * 1677 | * @param lockedByInstanceId the locking instance id 1678 | * @param currentState the current locking state 1679 | * @param newState the new trigger's state 1680 | */ 1681 | private void releaseLockedTriggers(String lockedByInstanceId, RedisTriggerState currentState, RedisTriggerState newState) { 1682 | try (Jedis jedis = pool.getResource()) { 1683 | lockPool.acquire(); 1684 | 1685 | Set triggerTuples = jedis.zrangeWithScores(currentState.getKey(), 0, -1); 1686 | for (Tuple triggerTuple : triggerTuples) { 1687 | if (lockedByInstanceId != null && lockedByInstanceId.equals(jedis.hget(triggerTuple.getElement(), LOCKED_BY))) { 1688 | log.debug("releasing trigger: " + triggerTuple.getElement() + " from: " + currentState.getKey()); 1689 | setTriggerState(newState, triggerTuple.getScore(), triggerTuple.getElement()); 1690 | } 1691 | } 1692 | } catch (Exception ex) { 1693 | log.error("could not release locked triggers in: " + currentState.getKey(), ex); 1694 | } finally { 1695 | lockPool.release(); 1696 | } 1697 | } 1698 | 1699 | private String readInstanceId(File instanceIdFile) { 1700 | String previousInstanceId = null; 1701 | InputStreamReader isr = null; 1702 | StringBuilder buffer = new StringBuilder(); 1703 | try { 1704 | if (instanceIdFile.exists()) { 1705 | FileInputStream fis = new FileInputStream(instanceIdFile); 1706 | isr = new InputStreamReader(fis, "UTF-8"); 1707 | Reader in = new BufferedReader(isr); 1708 | int ch; 1709 | while ((ch = in.read()) > -1) { 1710 | buffer.append((char)ch); 1711 | } 1712 | 1713 | previousInstanceId = buffer.toString(); 1714 | } 1715 | } catch (IOException ex) { 1716 | log.error("could not read previous scheduler instance id", ex); 1717 | } finally { 1718 | try { 1719 | if (isr != null) 1720 | isr.close(); 1721 | } catch (IOException ex) { 1722 | log.warn("could not close file reader", ex); 1723 | } 1724 | } 1725 | return previousInstanceId; 1726 | } 1727 | 1728 | private void writeInstanceId(File instanceIdFile) { 1729 | Writer out = null; 1730 | try { 1731 | FileOutputStream fos = new FileOutputStream(instanceIdFile, false); 1732 | out = new OutputStreamWriter(fos, "UTF-8"); 1733 | out.write(this.instanceId); 1734 | } catch (IOException ex) { 1735 | log.error("could not write new scheduler instance id", ex); 1736 | } finally { 1737 | try { 1738 | if (out != null) 1739 | out.close(); 1740 | } catch (IOException ex) { 1741 | log.warn("could not close file writer", ex); 1742 | } 1743 | } 1744 | } 1745 | 1746 | /** 1747 | * Release blocked jobs, blocked by the given instance id. 1748 | * 1749 | * @param blockedByInstanceId the blocking instance id 1750 | */ 1751 | private void releaseBlockedJobs(String blockedByInstanceId) { 1752 | try (Jedis jedis = pool.getResource()) { 1753 | lockPool.acquire(); 1754 | 1755 | Set blockedJobs = jedis.smembers(BLOCKED_JOBS_SET) ; 1756 | for (String blockedJob : blockedJobs) { 1757 | if (blockedByInstanceId != null && blockedByInstanceId.equals(jedis.hget(blockedJob, BLOCKED_BY))) { 1758 | jedis.hset(blockedJob, BLOCKED_BY, ""); 1759 | jedis.hset(blockedJob, BLOCK_TIME, ""); 1760 | jedis.srem(BLOCKED_JOBS_SET, blockedJob); 1761 | } 1762 | } 1763 | } catch (Exception ex) { 1764 | log.error("could not release blocked jobs", ex); 1765 | } finally { 1766 | lockPool.release(); 1767 | } 1768 | } 1769 | 1770 | @SuppressWarnings("unchecked") 1771 | private boolean isJobConcurrentExectionDisallowed(String jobClassName) { 1772 | boolean jobConcurrentExectionDisallowed = false; 1773 | try { 1774 | Class jobClass = (Class) loadHelper.getClassLoader().loadClass(jobClassName); 1775 | jobConcurrentExectionDisallowed = ClassUtils.isAnnotationPresent(jobClass, DisallowConcurrentExecution.class); 1776 | } catch (Exception ex) { 1777 | log.error("could not determine whether class: " + jobClassName + " is JobConcurrentExectionDisallowed annotated"); 1778 | } 1779 | return jobConcurrentExectionDisallowed; 1780 | } 1781 | 1782 | @SuppressWarnings("unchecked") 1783 | private boolean isPersistJobDataAfterExecution(String jobClassName) { 1784 | boolean persistJobDataAfterExecution = false; 1785 | try { 1786 | Class jobClass = (Class) loadHelper.getClassLoader().loadClass(jobClassName); 1787 | persistJobDataAfterExecution = ClassUtils.isAnnotationPresent(jobClass, PersistJobDataAfterExecution.class); 1788 | } catch (Exception ex) { 1789 | log.error("could not determine whether class: " + jobClassName + " is PersistJobDataAfterExecution annotated"); 1790 | } 1791 | return persistJobDataAfterExecution; 1792 | } 1793 | 1794 | @Override 1795 | public void releaseAcquiredTrigger(OperableTrigger trigger) { 1796 | try (Jedis jedis = pool.getResource()) { 1797 | lockPool.acquire(); 1798 | 1799 | String triggerHashKey = createTriggerHashKey(trigger.getKey().getGroup(), trigger.getKey().getName()); 1800 | if (jedis.zscore(RedisTriggerState.ACQUIRED.getKey(), triggerHashKey) != null) { 1801 | if (trigger.getNextFireTime() != null) 1802 | setTriggerState(RedisTriggerState.WAITING, (double)trigger.getNextFireTime().getTime(), triggerHashKey); 1803 | else 1804 | unsetTriggerState(triggerHashKey); 1805 | } 1806 | } catch(Exception ex) { 1807 | log.error("could not release acquired triggers", ex); 1808 | throw new RuntimeException(ex.getMessage(), ex.getCause()); 1809 | } finally { 1810 | lockPool.release(); 1811 | } 1812 | } 1813 | 1814 | @Override 1815 | public List triggersFired(List triggers) 1816 | throws JobPersistenceException { 1817 | List results = new ArrayList<>(); 1818 | try (Jedis jedis = pool.getResource()) { 1819 | lockPool.acquire(); 1820 | 1821 | for (OperableTrigger trigger : triggers) { 1822 | String triggerHashKey = createTriggerHashKey(trigger.getKey().getGroup(), trigger.getKey().getName()); 1823 | log.debug("trigger: " + triggerHashKey + " fired"); 1824 | 1825 | if (!jedis.exists(triggerHashKey)) 1826 | continue; // the trigger does not exist 1827 | 1828 | if (jedis.zscore(RedisTriggerState.ACQUIRED.getKey(), triggerHashKey) == null) 1829 | continue; // the trigger is not acquired 1830 | 1831 | Calendar cal = null; 1832 | if (trigger.getCalendarName() != null) { 1833 | String calendarName = trigger.getCalendarName(); 1834 | cal = retrieveCalendar(calendarName, jedis); 1835 | if(cal == null) 1836 | continue; 1837 | } 1838 | 1839 | Date prevFireTime = trigger.getPreviousFireTime(); 1840 | trigger.triggered(cal); 1841 | 1842 | TriggerFiredBundle bundle = new TriggerFiredBundle(retrieveJob(trigger.getJobKey(), jedis), trigger, cal, false, new Date(), trigger.getPreviousFireTime(), prevFireTime, trigger.getNextFireTime()); 1843 | 1844 | // handling job concurrent execution disallowed 1845 | String jobHashKey = createJobHashKey(trigger.getJobKey().getGroup(), trigger.getJobKey().getName()); 1846 | if (isJobConcurrentExectionDisallowed(jedis.hget(jobHashKey, JOB_CLASS))) { 1847 | String jobTriggerSetKey = createJobTriggersSetKey(trigger.getJobKey().getGroup(), trigger.getJobKey().getName()); 1848 | Set nonConcurrentTriggerHashKeys = jedis.smembers(jobTriggerSetKey); 1849 | for (String nonConcurrentTriggerHashKey : nonConcurrentTriggerHashKeys) { 1850 | Double score = jedis.zscore(RedisTriggerState.WAITING.getKey(), nonConcurrentTriggerHashKey); 1851 | if (score != null) { 1852 | setTriggerState(RedisTriggerState.BLOCKED, score, nonConcurrentTriggerHashKey); 1853 | } else { 1854 | score = jedis.zscore(RedisTriggerState.PAUSED.getKey(), nonConcurrentTriggerHashKey); 1855 | if (score != null) 1856 | setTriggerState(RedisTriggerState.PAUSED_BLOCKED, score, nonConcurrentTriggerHashKey); 1857 | } 1858 | } 1859 | 1860 | jedis.hset(jobHashKey, BLOCKED_BY, instanceId); 1861 | jedis.hset(jobHashKey, BLOCK_TIME, Long.toString(System.currentTimeMillis())); 1862 | jedis.sadd(BLOCKED_JOBS_SET, jobHashKey); 1863 | } 1864 | 1865 | // releasing the fired trigger 1866 | if (trigger.getNextFireTime() != null) { 1867 | jedis.hset(triggerHashKey, NEXT_FIRE_TIME, Long.toString(trigger.getNextFireTime().getTime())); 1868 | setTriggerState(RedisTriggerState.WAITING, (double)trigger.getNextFireTime().getTime(), triggerHashKey); 1869 | } else { 1870 | jedis.hset(triggerHashKey, NEXT_FIRE_TIME, ""); 1871 | unsetTriggerState(triggerHashKey); 1872 | } 1873 | 1874 | results.add(new TriggerFiredResult(bundle)); 1875 | } 1876 | } catch (JobPersistenceException | ClassNotFoundException | InterruptedException ex) { 1877 | log.error("could not acquire next triggers", ex); 1878 | throw new JobPersistenceException(ex.getMessage(), ex.getCause()); 1879 | } finally { 1880 | lockPool.release(); 1881 | } 1882 | return results; 1883 | } 1884 | 1885 | @Override 1886 | public void triggeredJobComplete(OperableTrigger trigger, 1887 | JobDetail jobDetail, CompletedExecutionInstruction triggerInstCode) { 1888 | String jobHashKey = createJobHashKey(jobDetail.getKey().getGroup(), jobDetail.getKey().getName()); 1889 | String jobDataMapHashKey = createJobDataMapHashKey(jobDetail.getKey().getGroup(), jobDetail.getKey().getName()); 1890 | String triggerHashKey = createTriggerHashKey(trigger.getKey().getGroup(), trigger.getKey().getName()); 1891 | log.debug("job: " + jobHashKey + " completed"); 1892 | try (Jedis jedis = pool.getResource()) { 1893 | lockPool.acquire(); 1894 | 1895 | if (jedis.exists(jobHashKey)) { // checking that the job wasn't deleted during the execution 1896 | String jobClassName = jedis.hget(jobHashKey, JOB_CLASS); 1897 | if (isPersistJobDataAfterExecution(jobClassName)) { 1898 | // updating the job data map 1899 | JobDataMap jobDataMap = jobDetail.getJobDataMap(); 1900 | 1901 | jedis.del(jobDataMapHashKey); 1902 | if (jobDataMap != null && !jobDataMap.isEmpty()) 1903 | jedis.hmset(jobDataMapHashKey, getStringDataMap(jobDataMap)); 1904 | } 1905 | 1906 | if (isJobConcurrentExectionDisallowed(jobClassName)) { 1907 | jedis.hset(jobHashKey, BLOCKED_BY, ""); 1908 | jedis.hset(jobHashKey, BLOCK_TIME, ""); 1909 | jedis.srem(BLOCKED_JOBS_SET, jobHashKey); 1910 | 1911 | String jobTriggersSetKey = createJobTriggersSetKey(trigger.getJobKey().getGroup(), trigger.getJobKey().getName()); 1912 | Set nonConcurrentTriggerHashKeys = jedis.smembers(jobTriggersSetKey); 1913 | for (String nonConcurrentTriggerHashkey : nonConcurrentTriggerHashKeys) { 1914 | Double score = jedis.zscore(RedisTriggerState.BLOCKED.getKey(), nonConcurrentTriggerHashkey); 1915 | if (score != null) { 1916 | setTriggerState(RedisTriggerState.WAITING, score, nonConcurrentTriggerHashkey); 1917 | } else { 1918 | score = jedis.zscore(RedisTriggerState.PAUSED_BLOCKED.getKey(), nonConcurrentTriggerHashkey); 1919 | if (score != null) 1920 | setTriggerState(RedisTriggerState.PAUSED, score, nonConcurrentTriggerHashkey); 1921 | } 1922 | } 1923 | 1924 | signaler.signalSchedulingChange(0L); 1925 | } 1926 | } else { // removing the job from blocked set anyway 1927 | jedis.srem(BLOCKED_JOBS_SET, jobHashKey); 1928 | } 1929 | 1930 | 1931 | if (jedis.exists(triggerHashKey)) { // checking that the trigger wasn't deleted during the execution 1932 | // applying the execution instruction for the completed job's trigger 1933 | if (triggerInstCode == CompletedExecutionInstruction.DELETE_TRIGGER) { 1934 | if (trigger.getNextFireTime() == null) { 1935 | // double check for possible reschedule within job execution, which would cancel the need to delete... 1936 | if (jedis.hget(triggerHashKey, NEXT_FIRE_TIME).isEmpty()) { 1937 | removeTrigger(trigger.getKey(), jedis); 1938 | } 1939 | } else { 1940 | removeTrigger(trigger.getKey(), jedis); 1941 | signaler.signalSchedulingChange(0L); 1942 | } 1943 | } else if (triggerInstCode == CompletedExecutionInstruction.SET_TRIGGER_COMPLETE) { 1944 | setTriggerState(RedisTriggerState.COMPLETED, (double)System.currentTimeMillis(), triggerHashKey); 1945 | signaler.signalSchedulingChange(0L); 1946 | } else if(triggerInstCode == CompletedExecutionInstruction.SET_TRIGGER_ERROR) { 1947 | log.debug("trigger: " + triggerHashKey + " ended with error"); 1948 | double score = trigger.getNextFireTime() != null ? (double)trigger.getNextFireTime().getTime() : 0D; 1949 | setTriggerState(RedisTriggerState.ERROR, score, triggerHashKey); 1950 | signaler.signalSchedulingChange(0L); 1951 | } else if (triggerInstCode == CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR) { 1952 | String jobTriggersSetKey = createJobTriggersSetKey(jobDetail.getKey().getGroup(), jobDetail.getKey().getName()); 1953 | Set errorTriggerHashkeys = jedis.smembers(jobTriggersSetKey); 1954 | for (String errorTriggerHashKey : errorTriggerHashkeys) { 1955 | double score = jedis.hget(errorTriggerHashKey, NEXT_FIRE_TIME).isEmpty() ? 0D : Double.parseDouble(jedis.hget(errorTriggerHashKey, NEXT_FIRE_TIME)); 1956 | setTriggerState(RedisTriggerState.ERROR, score, errorTriggerHashKey); 1957 | } 1958 | signaler.signalSchedulingChange(0L); 1959 | } else if (triggerInstCode == CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_COMPLETE) { 1960 | String jobTriggerHashkey = createJobTriggersSetKey(jobDetail.getKey().getGroup(), jobDetail.getKey().getName()); 1961 | Set comletedTriggerHashkeys = jedis.smembers(jobTriggerHashkey); 1962 | for (String completedTriggerHashKey : comletedTriggerHashkeys) { 1963 | setTriggerState(RedisTriggerState.COMPLETED, (double)System.currentTimeMillis(), completedTriggerHashKey); 1964 | } 1965 | signaler.signalSchedulingChange(0L); 1966 | } 1967 | } 1968 | } catch (JobPersistenceException | NumberFormatException | InterruptedException ex) { 1969 | log.error("could not handle triggegered job completion", ex); 1970 | throw new RuntimeException(ex.getMessage(), ex.getCause()); 1971 | } finally { 1972 | lockPool.release(); 1973 | } 1974 | } 1975 | 1976 | @Override 1977 | public void setInstanceId(String schedInstId) { 1978 | this.instanceId = schedInstId; 1979 | } 1980 | 1981 | @Override 1982 | public void setInstanceName(String schedName) { 1983 | // No-op 1984 | } 1985 | 1986 | @Override 1987 | public void setThreadPoolSize(int poolSize) { 1988 | // No-op 1989 | } 1990 | 1991 | protected boolean applyMisfire(OperableTrigger trigger, Jedis jedis) throws JobPersistenceException { 1992 | long misfireTime = System.currentTimeMillis(); 1993 | if (getMisfireThreshold() > 0) 1994 | misfireTime -= getMisfireThreshold(); 1995 | 1996 | Date triggerNextFireTime = trigger.getNextFireTime(); 1997 | if (triggerNextFireTime == null || triggerNextFireTime.getTime() > misfireTime 1998 | || trigger.getMisfireInstruction() == Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY) { 1999 | return false; 2000 | } 2001 | 2002 | Calendar cal = null; 2003 | if (trigger.getCalendarName() != null) 2004 | cal = retrieveCalendar(trigger.getCalendarName(), jedis); 2005 | 2006 | signaler.notifyTriggerListenersMisfired((OperableTrigger)trigger.clone()); 2007 | 2008 | trigger.updateAfterMisfire(cal); 2009 | 2010 | if (triggerNextFireTime.equals(trigger.getNextFireTime())) 2011 | return false; 2012 | 2013 | storeTrigger(trigger, true, jedis); 2014 | if (trigger.getNextFireTime() == null) { // Trigger completed 2015 | setTriggerState(RedisTriggerState.COMPLETED, (double)System.currentTimeMillis(), createTriggerHashKey(trigger.getKey().getGroup(), trigger.getKey().getName())); 2016 | signaler.notifySchedulerListenersFinalized(trigger); 2017 | } 2018 | 2019 | return true; 2020 | } 2021 | 2022 | private void keepAlive() { 2023 | try (Jedis jedis = pool.getResource()) { 2024 | jedis.set(instanceId, Long.toString(System.currentTimeMillis())); 2025 | } catch(Exception ex) { 2026 | log.error("could not keep alive"); 2027 | } 2028 | } 2029 | 2030 | public long getMisfireThreshold() { 2031 | return misfireThreshold; 2032 | } 2033 | 2034 | public void setMisfireThreshold(long misfireThreshold) { 2035 | if (misfireThreshold < 1) { 2036 | throw new IllegalArgumentException("misfire threshold must be larger than 0"); 2037 | } 2038 | 2039 | this.misfireThreshold = misfireThreshold; 2040 | } 2041 | 2042 | public String getInstanceId() { 2043 | return this.instanceId; 2044 | } 2045 | 2046 | public void setHost(String host) { 2047 | this.host = host; 2048 | } 2049 | 2050 | public void setPort(int port) { 2051 | this.port = port; 2052 | } 2053 | 2054 | public void setPassword(String password) { 2055 | this.password = password; 2056 | } 2057 | 2058 | public void setInstanceIdFilePath(String instanceIdFilePath) { 2059 | this.instanceIdFilePath = instanceIdFilePath; 2060 | } 2061 | 2062 | public void setReleaseTriggersInterval(int releaseTriggersInterval) { 2063 | this.releaseTriggersInterval = releaseTriggersInterval; 2064 | } 2065 | 2066 | public void setLockTimeout(int lockTimeout) { 2067 | this.lockTimeout = lockTimeout; 2068 | } 2069 | 2070 | class UnlockListener extends JedisPubSub { 2071 | 2072 | @Override 2073 | public void onMessage(String channel, String message) { 2074 | log.debug("message recieved: " + message + " on channel: " + channel); 2075 | if ("unlocked".equals(message) && UNLOCK_NOTIFICATIONS_CHANNEL.equals(channel)) 2076 | unsubscribe(channel); 2077 | } 2078 | 2079 | @Override 2080 | public void onPMessage(String pattern, String channel, String message) { 2081 | log.debug("pmessage recieved: " + message + " on channel: " + channel + ", pattern: " + pattern); 2082 | } 2083 | 2084 | @Override 2085 | public void onSubscribe(String channel, int subscribedChannels) { 2086 | log.debug("subscribing to channel: " + channel); 2087 | } 2088 | 2089 | @Override 2090 | public void onUnsubscribe(String channel, int subscribedChannels) { 2091 | log.debug("unsubscribing to channel: " + channel); 2092 | } 2093 | 2094 | @Override 2095 | public void onPUnsubscribe(String pattern, int subscribedChannels) { 2096 | log.debug("punsubscribing to channels pattern: " + pattern + ", subscribed channels: " + subscribedChannels); 2097 | } 2098 | 2099 | @Override 2100 | public void onPSubscribe(String pattern, int subscribedChannels) { 2101 | log.debug("psubscribing to channels pattern: " + pattern + ", subscribed channels: " + subscribedChannels); 2102 | } 2103 | } 2104 | } -------------------------------------------------------------------------------- /src/main/java/com/redislabs/quartz/RedisTriggerState.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.quartz; 2 | 3 | public enum RedisTriggerState { 4 | WAITING("waiting_triggers"), 5 | PAUSED("paused_triggers"), 6 | BLOCKED("blocked_triggers"), 7 | PAUSED_BLOCKED("paused_blocked_triggers"), 8 | ACQUIRED("acquired_triggers"), 9 | COMPLETED("completed_triggers"), 10 | ERROR("error_triggers"); 11 | 12 | private final String key; 13 | 14 | private RedisTriggerState(String key) { 15 | this.key = key; 16 | } 17 | 18 | public String getKey() { return key; } 19 | 20 | public static RedisTriggerState toState(String key){ 21 | for (RedisTriggerState state : RedisTriggerState.values()) 22 | if (state.getKey().equals(key)) 23 | return state; 24 | 25 | return null; 26 | } 27 | } --------------------------------------------------------------------------------