├── .circleci └── config.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── changelog.md ├── pom.xml └── src ├── main └── java │ └── net │ └── joelinn │ └── quartz │ └── jobstore │ ├── AbstractRedisStorage.java │ ├── RedisClusterStorage.java │ ├── RedisJobStore.java │ ├── RedisJobStoreSchema.java │ ├── RedisStorage.java │ ├── RedisTriggerState.java │ ├── jedis │ └── JedisClusterCommandsWrapper.java │ └── mixin │ ├── CronTriggerMixin.java │ ├── HolidayCalendarMixin.java │ ├── JobDetailMixin.java │ └── TriggerMixin.java └── test ├── java └── net │ └── joelinn │ ├── junit │ ├── Retry.java │ └── RetryRule.java │ └── quartz │ ├── BaseIntegrationTest.java │ ├── BaseTest.java │ ├── MultiSchedulerIntegrationTest.java │ ├── MultiThreadedIntegrationTest.java │ ├── RedisJobStoreTest.java │ ├── RedisSentinelJobStoreTest.java │ ├── SingleThreadedIntegrationTest.java │ ├── StoreCalendarTest.java │ ├── StoreJobTest.java │ ├── StoreTriggerTest.java │ ├── TestJob.java │ ├── TestJobNonConcurrent.java │ ├── TestJobPersist.java │ ├── TestUtils.java │ ├── TriggeredJobCompleteTest.java │ └── mixin │ ├── CronTriggerMixinTest.java │ ├── JobDetailMixinTest.java │ └── SimpleTriggerMixinTest.java └── resources └── logback-test.xml /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | maven: circleci/maven@1.1 5 | 6 | workflows: 7 | maven_test: 8 | jobs: 9 | - maven/test 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | 4 | target/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | dist: trusty 3 | jdk: 4 | - openjdk7 5 | - openjdk8 6 | - oraclejdk8 7 | - oraclejdk9 8 | 9 | sudo: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | quartz-redis-jobstore 2 | ===================== 3 | 4 | [![Build Status](https://secure.travis-ci.org/jlinn/quartz-redis-jobstore.png?branch=master)](http://travis-ci.org/jlinn/quartz-redis-jobstore) 5 | 6 | A [Quartz Scheduler](http://quartz-scheduler.org/) JobStore using [Redis](http://redis.io). 7 | 8 | This project was inspired by [redis-quartz](https://github.com/RedisLabs/redis-quartz), and provides similar functionality with some key differences: 9 | 10 | * Redis database and key prefix are configurable. 11 | * Redis' [recommended distributed locking method](http://redis.io/topics/distlock) is used. 12 | * All of the functionality of this library is covered by a [test suite](https://github.com/jlinn/quartz-redis-jobstore/tree/master/src/test/java/net/joelinn/quartz). 13 | 14 | ## Requirements 15 | * Java 7 or higher 16 | * Redis 2.6.12 or higher (3.0 or higher for Redis cluster) 17 | 18 | ## Installation 19 | Maven dependency: 20 | ```xml 21 | 22 | net.joelinn 23 | quartz-redis-jobstore 24 | 1.2.0 25 | 26 | ``` 27 | 28 | ## Configuration 29 | The following properties may be set in your `quartz.properties` file: 30 | ``` 31 | # set the scheduler's JobStore class (required) 32 | org.quartz.jobStore.class = net.joelinn.quartz.jobstore.RedisJobStore 33 | 34 | # set the Redis host (required) 35 | org.quartz.jobStore.host = 36 | 37 | # set the scheduler's trigger misfire threshold in milliseconds (optional, defaults to 60000) 38 | org.quartz.jobStore.misfireThreshold = 60000 39 | 40 | # set the redis password (optional, defaults null) 41 | org.quartz.jobStore.password = 42 | 43 | # set the redis port (optional, defaults to 6379) 44 | org.quartz.jobStore.port = 45 | 46 | # enable Redis clustering (optional, defaults to false) 47 | org.quartz.jobStore.redisCluster = 48 | 49 | # enable Redis sentinel (optional, defaults to false) 50 | org.quartz.jobStore.redisSentinel = 51 | 52 | # set the sentinel master group name (required if redisSentinel = true) 53 | org.quartz.jobStore.masterGroupName = 54 | 55 | # set the redis database (optional, defaults to 0) 56 | org.quartz.jobStore.database: 57 | 58 | # set the Redis key prefix for all JobStore Redis keys (optional, defaults to none) 59 | org.quartz.jobStore.keyPrefix = a_prefix_ 60 | 61 | # set the Redis lock timeout in milliseconds (optional, defaults to 30000) 62 | org.quartz.jobStore.lockTimeout = 30000 63 | 64 | # enable SSL (defaults to false) 65 | org.quartz.jobStore.ssl = 66 | ``` 67 | 68 | ## Limitations 69 | All GroupMatcher comparators have been implemented. 70 | Aside from that, the same limitations outlined in [redis-quartz's readme](https://github.com/RedisLabs/redis-quartz#limitations) apply. 71 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ### 2019-07-02 3 | * Upgrade to Jedis 3.0.1 4 | 5 | ### 2019-06-26 6 | * Delete job data map set from Redis prior to storing new job data when updating / overwriting a job. 7 | This will prevent keys which were removed from the job's data map prior to storage from being preserved. 8 | 9 | ### 2018-03-01 10 | * Detect dead schedulers and unblock their blocked triggers 11 | * Keep track of `previousFireTime` for triggers 12 | 13 | ### 2018-02-27 14 | * Set fire instance id on retrieved triggers 15 | * Fixed a bug where trigger locks would get incorrectly removed for non-concurrent jobs 16 | 17 | ### 2016-12-30 18 | * Fix a bug when handling trigger firing for triggers with no next fire time 19 | 20 | ### 2016-12-04 21 | * Fixed handling of jobs marked with `@DisallowConcurrentExecution`. 22 | 23 | ### 2016-10-30 24 | * Fix serialization of HolidayCalendar 25 | 26 | ### 2016-10-23 27 | * Add support for storing trigger-specific job data 28 | 29 | ### 2016-05-04 30 | * Add support for Redis password 31 | 32 | ### 2016-03-17 33 | * Allow Redis db to be set when using Sentinel 34 | 35 | ### 2016-03-02 36 | * Fix a bug where acquired triggers were not being released. 37 | 38 | ### 2016-01-31 39 | * Add support for Redis Sentinel 40 | 41 | ### 2015-08-19 42 | * Add support for [Jedis cluster](https://github.com/xetorthio/jedis#jedis-cluster). 43 | * Allow a pre-configured Pool or JedisCluster to be passed in to RedisJobStore. 44 | * Update to Jackson v2.6.1. 45 | 46 | ### 2014-12-09 47 | * Remove Guava dependency 48 | 49 | ### 2014-09-24 50 | * Add the ability to specify a redis database. 51 | * Fix setter methods for `keyPrefix` and `keyDelimiter` properties. 52 | * Set default port to 6379. 53 | 54 | ### 2014-08-21 55 | * Fix a bug where non-durable jobs with only one trigger would be deleted when replaceTrigger() was called with that trigger. 56 | 57 | ### 2014-07-25 58 | * Handle `ObjectAlreadyExistsException` separately in RedisJobStore::storeJobAndTrigger() 59 | 60 | ### 2014-07-24 61 | * Enable the use of all GroupMatchers (not just EQUALS) -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | net.joelinn 6 | quartz-redis-jobstore 7 | 1.2.1-SNAPSHOT 8 | jar 9 | quartz-redis-jobstore 10 | A Quartz Scheduler JobStore using Redis. 11 | https://github.com/jlinn/quartz-redis-jobstore 12 | 13 | 14 | 15 | The Apache Software License, Version 2.0 16 | http://www.apache.org/licenses/LICENSE-2.0.txt 17 | repo 18 | 19 | 20 | 21 | 22 | 23 | Joe Linn 24 | https://github.com/jlinn 25 | 26 | 27 | 28 | 29 | scm:git:git@github.com:jlinn/quartz-redis-jobstore.git 30 | scm:git:git@github.com:jlinn/quartz-redis-jobstore.git 31 | http://github.com/jlinn/quartz-redis-jobstore 32 | HEAD 33 | 34 | 35 | 36 | UTF-8 37 | -Xdoclint:none 38 | 2.2.1 39 | 2.11.1 40 | 1.1.7 41 | 42 | 43 | 44 | 45 | clojars.org 46 | http://clojars.org/repo 47 | 48 | 49 | 50 | 51 | 52 | org.quartz-scheduler 53 | quartz 54 | ${quartz.version} 55 | 56 | 57 | 58 | org.quartz-scheduler 59 | quartz-jobs 60 | ${quartz.version} 61 | 62 | 63 | 64 | redis.clients 65 | jedis 66 | 3.3.0 67 | 68 | 69 | 70 | com.fasterxml.jackson.core 71 | jackson-core 72 | ${jackson.version} 73 | 74 | 75 | 76 | com.fasterxml.jackson.core 77 | jackson-annotations 78 | ${jackson.version} 79 | 80 | 81 | 82 | com.fasterxml.jackson.core 83 | jackson-databind 84 | ${jackson.version} 85 | 86 | 87 | 88 | org.slf4j 89 | slf4j-api 90 | 1.7.7 91 | 92 | 93 | 94 | 95 | junit 96 | junit 97 | 4.12 98 | test 99 | 100 | 101 | 102 | org.hamcrest 103 | hamcrest-all 104 | 1.3 105 | test 106 | 107 | 108 | 109 | org.mockito 110 | mockito-all 111 | 1.9.5 112 | test 113 | 114 | 115 | 116 | com.google.guava 117 | guava-io 118 | r03 119 | test 120 | 121 | 122 | 123 | commons-io 124 | commons-io 125 | 2.4 126 | test 127 | 128 | 129 | 130 | com.github.kstyrc 131 | embedded-redis 132 | 0.6 133 | test 134 | 135 | 136 | 137 | net.jodah 138 | concurrentunit 139 | 0.4.2 140 | test 141 | 142 | 143 | 144 | ch.qos.logback 145 | logback-classic 146 | ${logback.version} 147 | test 148 | 149 | 150 | 151 | ch.qos.logback 152 | logback-core 153 | ${logback.version} 154 | test 155 | 156 | 157 | 158 | 159 | 160 | 161 | src/test/resources 162 | 163 | 164 | 165 | 166 | 167 | org.apache.maven.plugins 168 | maven-source-plugin 169 | 2.3 170 | 171 | 172 | attach-sources 173 | 174 | jar 175 | 176 | 177 | 178 | 179 | 180 | 181 | maven-assembly-plugin 182 | 183 | 184 | package 185 | 186 | attached 187 | 188 | 189 | 190 | 191 | 192 | jar-with-dependencies 193 | 194 | 195 | 196 | 197 | 198 | org.apache.maven.plugins 199 | maven-release-plugin 200 | 2.5 201 | 202 | -Dgpg.passphrase=${gpg.passphrase} 203 | deploy -Dmaven.test.skip=true 204 | 205 | 206 | 207 | 208 | org.apache.maven.plugins 209 | maven-surefire-plugin 210 | 2.17 211 | 212 | false 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | org.apache.maven.plugins 221 | maven-compiler-plugin 222 | 223 | 1.7 224 | 1.7 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | release-sign-artifacts 234 | 235 | 236 | performRelease 237 | true 238 | 239 | 240 | 241 | 242 | 243 | org.apache.maven.plugins 244 | maven-gpg-plugin 245 | 1.5 246 | 247 | ${gpg.passphrase} 248 | 249 | 250 | 251 | sign-artifacts 252 | verify 253 | 254 | sign 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | sonatype-nexus-snapshots 267 | Sonatype Nexus snapshot repository 268 | https://oss.sonatype.org/content/repositories/snapshots 269 | 270 | 271 | sonatype-nexus-staging 272 | Sonatype Nexus release repository 273 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 274 | 275 | 276 | 277 | -------------------------------------------------------------------------------- /src/main/java/net/joelinn/quartz/jobstore/RedisClusterStorage.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.jobstore; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import net.joelinn.quartz.jobstore.jedis.JedisClusterCommandsWrapper; 7 | import org.quartz.Calendar; 8 | import org.quartz.*; 9 | import org.quartz.impl.matchers.GroupMatcher; 10 | import org.quartz.impl.matchers.StringMatcher; 11 | import org.quartz.spi.OperableTrigger; 12 | import org.quartz.spi.SchedulerSignaler; 13 | import org.quartz.spi.TriggerFiredBundle; 14 | import org.quartz.spi.TriggerFiredResult; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | import java.util.*; 19 | 20 | /** 21 | * Joe Linn 22 | * 8/22/2015 23 | */ 24 | public class RedisClusterStorage extends AbstractRedisStorage { 25 | private static final Logger logger = LoggerFactory.getLogger(RedisClusterStorage.class); 26 | 27 | public RedisClusterStorage(RedisJobStoreSchema redisSchema, ObjectMapper mapper, SchedulerSignaler signaler, String schedulerInstanceId, int lockTimeout) { 28 | super(redisSchema, mapper, signaler, schedulerInstanceId, lockTimeout); 29 | } 30 | 31 | /** 32 | * Store a job in Redis 33 | * 34 | * @param jobDetail the {@link JobDetail} object to be stored 35 | * @param replaceExisting if true, any existing job with the same group and name as the given job will be overwritten 36 | * @param jedis a thread-safe Redis connection 37 | * @throws ObjectAlreadyExistsException 38 | */ 39 | @Override 40 | @SuppressWarnings("unchecked") 41 | public void storeJob(JobDetail jobDetail, boolean replaceExisting, JedisClusterCommandsWrapper jedis) throws ObjectAlreadyExistsException { 42 | final String jobHashKey = redisSchema.jobHashKey(jobDetail.getKey()); 43 | final String jobDataMapHashKey = redisSchema.jobDataMapHashKey(jobDetail.getKey()); 44 | final String jobGroupSetKey = redisSchema.jobGroupSetKey(jobDetail.getKey()); 45 | 46 | if (!replaceExisting && jedis.exists(jobHashKey)) { 47 | throw new ObjectAlreadyExistsException(jobDetail); 48 | } 49 | 50 | jedis.hmset(jobHashKey, (Map) mapper.convertValue(jobDetail, new TypeReference>() { 51 | })); 52 | jedis.del(jobDataMapHashKey); 53 | if (jobDetail.getJobDataMap() != null && !jobDetail.getJobDataMap().isEmpty()) { 54 | jedis.hmset(jobDataMapHashKey, getStringDataMap(jobDetail.getJobDataMap())); 55 | } 56 | 57 | jedis.sadd(redisSchema.jobsSet(), jobHashKey); 58 | jedis.sadd(redisSchema.jobGroupsSet(), jobGroupSetKey); 59 | jedis.sadd(jobGroupSetKey, jobHashKey); 60 | } 61 | 62 | /** 63 | * Remove the given job from Redis 64 | * 65 | * @param jobKey the job to be removed 66 | * @param jedis a thread-safe Redis connection 67 | * @return true if the job was removed; false if it did not exist 68 | */ 69 | @Override 70 | public boolean removeJob(JobKey jobKey, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 71 | final String jobHashKey = redisSchema.jobHashKey(jobKey); 72 | final String jobBlockedKey = redisSchema.jobBlockedKey(jobKey); 73 | final String jobDataMapHashKey = redisSchema.jobDataMapHashKey(jobKey); 74 | final String jobGroupSetKey = redisSchema.jobGroupSetKey(jobKey); 75 | final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(jobKey); 76 | 77 | // remove the job and any associated data 78 | Long delJobHashKeyResponse = jedis.del(jobHashKey); 79 | // remove the blocked job key 80 | jedis.del(jobBlockedKey); 81 | // remove the job's data map 82 | jedis.del(jobDataMapHashKey); 83 | // remove the job from the set of all jobs 84 | jedis.srem(redisSchema.jobsSet(), jobHashKey); 85 | // remove the job from the set of blocked jobs 86 | jedis.srem(redisSchema.blockedJobsSet(), jobHashKey); 87 | // remove the job from its group 88 | jedis.srem(jobGroupSetKey, jobHashKey); 89 | // retrieve the keys for all triggers associated with this job, then delete that set 90 | Set jobTriggerSetResponse = jedis.smembers(jobTriggerSetKey); 91 | jedis.del(jobTriggerSetKey); 92 | Long jobGroupSetSizeResponse = jedis.scard(jobGroupSetKey); 93 | if (jobGroupSetSizeResponse == 0) { 94 | // The group now contains no jobs. Remove it from the set of all job groups. 95 | jedis.srem(redisSchema.jobGroupsSet(), jobGroupSetKey); 96 | } 97 | 98 | // remove all triggers associated with this job 99 | for (String triggerHashKey : jobTriggerSetResponse) { 100 | // get this trigger's TriggerKey 101 | final TriggerKey triggerKey = redisSchema.triggerKey(triggerHashKey); 102 | final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(triggerKey); 103 | unsetTriggerState(triggerHashKey, jedis); 104 | // remove the trigger from the set of all triggers 105 | jedis.srem(redisSchema.triggersSet(), triggerHashKey); 106 | // remove the trigger's group from the set of all trigger groups 107 | jedis.srem(redisSchema.triggerGroupsSet(), triggerGroupSetKey); 108 | // remove this trigger from its group 109 | jedis.srem(triggerGroupSetKey, triggerHashKey); 110 | // delete the trigger 111 | jedis.del(triggerHashKey); 112 | } 113 | return delJobHashKeyResponse == 1; 114 | } 115 | 116 | /** 117 | * Store a trigger in redis 118 | * 119 | * @param trigger the trigger to be stored 120 | * @param replaceExisting true if an existing trigger with the same identity should be replaced 121 | * @param jedis a thread-safe Redis connection 122 | * @throws JobPersistenceException 123 | * @throws ObjectAlreadyExistsException 124 | */ 125 | @Override 126 | public void storeTrigger(OperableTrigger trigger, boolean replaceExisting, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 127 | final String triggerHashKey = redisSchema.triggerHashKey(trigger.getKey()); 128 | final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(trigger.getKey()); 129 | final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(trigger.getJobKey()); 130 | 131 | if (!(trigger instanceof SimpleTrigger) && !(trigger instanceof CronTrigger)) { 132 | throw new UnsupportedOperationException("Only SimpleTrigger and CronTrigger are supported."); 133 | } 134 | final boolean exists = jedis.exists(triggerHashKey); 135 | if (exists && !replaceExisting) { 136 | throw new ObjectAlreadyExistsException(trigger); 137 | } 138 | 139 | Map triggerMap = mapper.convertValue(trigger, new TypeReference>() { 140 | }); 141 | triggerMap.put(TRIGGER_CLASS, trigger.getClass().getName()); 142 | 143 | jedis.hmset(triggerHashKey, triggerMap); 144 | jedis.sadd(redisSchema.triggersSet(), triggerHashKey); 145 | jedis.sadd(redisSchema.triggerGroupsSet(), triggerGroupSetKey); 146 | jedis.sadd(triggerGroupSetKey, triggerHashKey); 147 | jedis.sadd(jobTriggerSetKey, triggerHashKey); 148 | if (trigger.getCalendarName() != null && !trigger.getCalendarName().isEmpty()) { 149 | final String calendarTriggersSetKey = redisSchema.calendarTriggersSetKey(trigger.getCalendarName()); 150 | jedis.sadd(calendarTriggersSetKey, triggerHashKey); 151 | } 152 | if (trigger.getJobDataMap() != null && !trigger.getJobDataMap().isEmpty()) { 153 | final String triggerDataMapHashKey = redisSchema.triggerDataMapHashKey(trigger.getKey()); 154 | jedis.hmset(triggerDataMapHashKey, getStringDataMap(trigger.getJobDataMap())); 155 | } 156 | 157 | if (exists) { 158 | // We're overwriting a previously stored instance of this trigger, so clear any existing trigger state. 159 | unsetTriggerState(triggerHashKey, jedis); 160 | } 161 | 162 | Boolean triggerPausedResponse = jedis.sismember(redisSchema.pausedTriggerGroupsSet(), triggerGroupSetKey); 163 | Boolean jobPausedResponse = jedis.sismember(redisSchema.pausedJobGroupsSet(), redisSchema.jobGroupSetKey(trigger.getJobKey())); 164 | 165 | if (triggerPausedResponse || jobPausedResponse) { 166 | final long nextFireTime = trigger.getNextFireTime() != null ? trigger.getNextFireTime().getTime() : -1; 167 | final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey()); 168 | if (isBlockedJob(jobHashKey, jedis)) { 169 | setTriggerState(RedisTriggerState.PAUSED_BLOCKED, (double) nextFireTime, triggerHashKey, jedis); 170 | } else { 171 | setTriggerState(RedisTriggerState.PAUSED, (double) nextFireTime, triggerHashKey, jedis); 172 | } 173 | } else if (trigger.getNextFireTime() != null) { 174 | setTriggerState(RedisTriggerState.WAITING, (double) trigger.getNextFireTime().getTime(), triggerHashKey, jedis); 175 | } 176 | } 177 | 178 | /** 179 | * Remove (delete) the {@link Trigger} with the given key. 180 | * 181 | * @param triggerKey the key of the trigger to be removed 182 | * @param removeNonDurableJob if true, the job associated with the given trigger will be removed if it is non-durable 183 | * and has no other triggers 184 | * @param jedis a thread-safe Redis connection 185 | * @return true if the trigger was found and removed 186 | */ 187 | @Override 188 | protected boolean removeTrigger(TriggerKey triggerKey, boolean removeNonDurableJob, JedisClusterCommandsWrapper jedis) throws JobPersistenceException, ClassNotFoundException { 189 | final String triggerHashKey = redisSchema.triggerHashKey(triggerKey); 190 | final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(triggerKey); 191 | 192 | if (!jedis.exists(triggerHashKey)) { 193 | return false; 194 | } 195 | 196 | OperableTrigger trigger = retrieveTrigger(triggerKey, jedis); 197 | 198 | final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey()); 199 | final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(trigger.getJobKey()); 200 | 201 | // remove the trigger from the set of all triggers 202 | jedis.srem(redisSchema.triggersSet(), triggerHashKey); 203 | // remove the trigger from its trigger group set 204 | jedis.srem(triggerGroupSetKey, triggerHashKey); 205 | // remove the trigger from the associated job's trigger set 206 | jedis.srem(jobTriggerSetKey, triggerHashKey); 207 | 208 | if (jedis.scard(triggerGroupSetKey) == 0) { 209 | // The trigger group set is empty. Remove the trigger group from the set of trigger groups. 210 | jedis.srem(redisSchema.triggerGroupsSet(), triggerGroupSetKey); 211 | } 212 | 213 | if (removeNonDurableJob) { 214 | Long jobTriggerSetKeySizeResponse = jedis.scard(jobTriggerSetKey); 215 | Boolean jobExistsResponse = jedis.exists(jobHashKey); 216 | if (jobTriggerSetKeySizeResponse == 0 && jobExistsResponse) { 217 | JobDetail job = retrieveJob(trigger.getJobKey(), jedis); 218 | if (!job.isDurable()) { 219 | // Job is not durable and has no remaining triggers. Delete it. 220 | removeJob(job.getKey(), jedis); 221 | signaler.notifySchedulerListenersJobDeleted(job.getKey()); 222 | } 223 | } 224 | } 225 | 226 | if (isNullOrEmpty(trigger.getCalendarName())) { 227 | jedis.srem(redisSchema.calendarTriggersSetKey(trigger.getCalendarName()), triggerHashKey); 228 | } 229 | unsetTriggerState(triggerHashKey, jedis); 230 | jedis.del(triggerHashKey); 231 | return true; 232 | } 233 | 234 | /** 235 | * Unsets the state of the given trigger key by removing the trigger from all trigger state sets. 236 | * 237 | * @param triggerHashKey the redis key of the desired trigger hash 238 | * @param jedis a thread-safe Redis connection 239 | * @return true if the trigger was removed, false if the trigger was stateless 240 | * @throws JobPersistenceException if the unset operation failed 241 | */ 242 | @Override 243 | public boolean unsetTriggerState(String triggerHashKey, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 244 | boolean removed = false; 245 | List responses = new ArrayList<>(RedisTriggerState.values().length); 246 | for (RedisTriggerState state : RedisTriggerState.values()) { 247 | responses.add(jedis.zrem(redisSchema.triggerStateKey(state), triggerHashKey)); 248 | } 249 | for (Long response : responses) { 250 | removed = response == 1; 251 | if (removed) { 252 | jedis.del(redisSchema.triggerLockKey(redisSchema.triggerKey(triggerHashKey))); 253 | break; 254 | } 255 | } 256 | return removed; 257 | } 258 | 259 | /** 260 | * Store a {@link Calendar} 261 | * 262 | * @param name the name of the calendar 263 | * @param calendar the calendar object to be stored 264 | * @param replaceExisting if true, any existing calendar with the same name will be overwritten 265 | * @param updateTriggers if true, any existing triggers associated with the calendar will be updated 266 | * @param jedis a thread-safe Redis connection 267 | * @throws JobPersistenceException 268 | */ 269 | @Override 270 | public void storeCalendar(String name, Calendar calendar, boolean replaceExisting, boolean updateTriggers, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 271 | final String calendarHashKey = redisSchema.calendarHashKey(name); 272 | if (!replaceExisting && jedis.exists(calendarHashKey)) { 273 | throw new ObjectAlreadyExistsException(String.format("Calendar with key %s already exists.", calendarHashKey)); 274 | } 275 | Map calendarMap = new HashMap<>(); 276 | calendarMap.put(CALENDAR_CLASS, calendar.getClass().getName()); 277 | try { 278 | calendarMap.put(CALENDAR_JSON, mapper.writeValueAsString(calendar)); 279 | } catch (JsonProcessingException e) { 280 | throw new JobPersistenceException("Unable to serialize calendar.", e); 281 | } 282 | 283 | jedis.hmset(calendarHashKey, calendarMap); 284 | jedis.sadd(redisSchema.calendarsSet(), calendarHashKey); 285 | 286 | if (updateTriggers) { 287 | final String calendarTriggersSetKey = redisSchema.calendarTriggersSetKey(name); 288 | Set triggerHashKeys = jedis.smembers(calendarTriggersSetKey); 289 | for (String triggerHashKey : triggerHashKeys) { 290 | OperableTrigger trigger = retrieveTrigger(redisSchema.triggerKey(triggerHashKey), jedis); 291 | long removed = jedis.zrem(redisSchema.triggerStateKey(RedisTriggerState.WAITING), triggerHashKey); 292 | trigger.updateWithNewCalendar(calendar, misfireThreshold); 293 | if (removed == 1) { 294 | setTriggerState(RedisTriggerState.WAITING, (double) trigger.getNextFireTime().getTime(), triggerHashKey, jedis); 295 | } 296 | } 297 | } 298 | } 299 | 300 | /** 301 | * Remove (delete) the {@link Calendar} with the given name. 302 | * 303 | * @param calendarName the name of the calendar to be removed 304 | * @param jedis a thread-safe Redis connection 305 | * @return true if a calendar with the given name was found and removed 306 | */ 307 | @Override 308 | public boolean removeCalendar(String calendarName, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 309 | final String calendarTriggersSetKey = redisSchema.calendarTriggersSetKey(calendarName); 310 | 311 | if (jedis.scard(calendarTriggersSetKey) > 0) { 312 | throw new JobPersistenceException(String.format("There are triggers pointing to calendar %s, so it cannot be removed.", calendarName)); 313 | } 314 | final String calendarHashKey = redisSchema.calendarHashKey(calendarName); 315 | Long deleteResponse = jedis.del(calendarHashKey); 316 | jedis.srem(redisSchema.calendarsSet(), calendarHashKey); 317 | 318 | return deleteResponse == 1; 319 | } 320 | 321 | /** 322 | * Get the keys of all of the {@link Job} s that have the given group name. 323 | * 324 | * @param matcher the matcher with which to compare group names 325 | * @param jedis a thread-safe Redis connection 326 | * @return the set of all JobKeys which have the given group name 327 | */ 328 | @Override 329 | public Set getJobKeys(GroupMatcher matcher, JedisClusterCommandsWrapper jedis) { 330 | Set jobKeys = new HashSet<>(); 331 | if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) { 332 | final String jobGroupSetKey = redisSchema.jobGroupSetKey(new JobKey("", matcher.getCompareToValue())); 333 | final Set jobs = jedis.smembers(jobGroupSetKey); 334 | if (jobs != null) { 335 | for (final String job : jobs) { 336 | jobKeys.add(redisSchema.jobKey(job)); 337 | } 338 | } 339 | } else { 340 | List> jobGroups = new ArrayList<>(); 341 | for (final String jobGroupSetKey : jedis.smembers(redisSchema.jobGroupsSet())) { 342 | if (matcher.getCompareWithOperator().evaluate(redisSchema.jobGroup(jobGroupSetKey), matcher.getCompareToValue())) { 343 | jobGroups.add(jedis.smembers(jobGroupSetKey)); 344 | } 345 | } 346 | for (Set jobGroup : jobGroups) { 347 | if (jobGroup != null) { 348 | for (final String job : jobGroup) { 349 | jobKeys.add(redisSchema.jobKey(job)); 350 | } 351 | } 352 | } 353 | } 354 | return jobKeys; 355 | } 356 | 357 | /** 358 | * Get the names of all of the {@link Trigger} s that have the given group name. 359 | * 360 | * @param matcher the matcher with which to compare group names 361 | * @param jedis a thread-safe Redis connection 362 | * @return the set of all TriggerKeys which have the given group name 363 | */ 364 | @Override 365 | public Set getTriggerKeys(GroupMatcher matcher, JedisClusterCommandsWrapper jedis) { 366 | Set triggerKeys = new HashSet<>(); 367 | if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) { 368 | final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(new TriggerKey("", matcher.getCompareToValue())); 369 | final Set triggers = jedis.smembers(triggerGroupSetKey); 370 | if (triggers != null) { 371 | for (final String trigger : triggers) { 372 | triggerKeys.add(redisSchema.triggerKey(trigger)); 373 | } 374 | } 375 | } else { 376 | List> triggerGroups = new ArrayList<>(); 377 | for (final String triggerGroupSetKey : jedis.smembers(redisSchema.triggerGroupsSet())) { 378 | if (matcher.getCompareWithOperator().evaluate(redisSchema.triggerGroup(triggerGroupSetKey), matcher.getCompareToValue())) { 379 | triggerGroups.add(jedis.smembers(triggerGroupSetKey)); 380 | } 381 | } 382 | for (Set triggerGroup : triggerGroups) { 383 | if (triggerGroup != null) { 384 | for (final String trigger : triggerGroup) { 385 | triggerKeys.add(redisSchema.triggerKey(trigger)); 386 | } 387 | } 388 | } 389 | } 390 | return triggerKeys; 391 | } 392 | 393 | /** 394 | * Get the current state of the identified {@link Trigger}. 395 | * 396 | * @param triggerKey the key of the desired trigger 397 | * @param jedis a thread-safe Redis connection 398 | * @return the state of the trigger 399 | */ 400 | @Override 401 | public Trigger.TriggerState getTriggerState(TriggerKey triggerKey, JedisClusterCommandsWrapper jedis) { 402 | final String triggerHashKey = redisSchema.triggerHashKey(triggerKey); 403 | Map scores = new HashMap<>(RedisTriggerState.values().length); 404 | for (RedisTriggerState redisTriggerState : RedisTriggerState.values()) { 405 | scores.put(redisTriggerState, jedis.zscore(redisSchema.triggerStateKey(redisTriggerState), triggerHashKey)); 406 | } 407 | for (Map.Entry entry : scores.entrySet()) { 408 | if (entry.getValue() != null) { 409 | return entry.getKey().getTriggerState(); 410 | } 411 | } 412 | return Trigger.TriggerState.NONE; 413 | } 414 | 415 | /** 416 | * Pause the trigger with the given key 417 | * 418 | * @param triggerKey the key of the trigger to be paused 419 | * @param jedis a thread-safe Redis connection 420 | * @throws JobPersistenceException if the desired trigger does not exist 421 | */ 422 | @Override 423 | public void pauseTrigger(TriggerKey triggerKey, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 424 | final String triggerHashKey = redisSchema.triggerHashKey(triggerKey); 425 | Boolean exists = jedis.exists(triggerHashKey); 426 | Double completedScore = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.COMPLETED), triggerHashKey); 427 | String nextFireTimeResponse = jedis.hget(triggerHashKey, TRIGGER_NEXT_FIRE_TIME); 428 | Double blockedScore = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.BLOCKED), triggerHashKey); 429 | 430 | if (!exists) { 431 | return; 432 | } 433 | if (completedScore != null) { 434 | // doesn't make sense to pause a completed trigger 435 | return; 436 | } 437 | 438 | final long nextFireTime = nextFireTimeResponse == null 439 | || nextFireTimeResponse.isEmpty() ? -1 : Long.parseLong(nextFireTimeResponse); 440 | if (blockedScore != null) { 441 | setTriggerState(RedisTriggerState.PAUSED_BLOCKED, (double) nextFireTime, triggerHashKey, jedis); 442 | } else { 443 | setTriggerState(RedisTriggerState.PAUSED, (double) nextFireTime, triggerHashKey, jedis); 444 | } 445 | } 446 | 447 | /** 448 | * Pause all of the {@link Trigger}s in the given group. 449 | * 450 | * @param matcher matcher for the trigger groups to be paused 451 | * @param jedis a thread-safe Redis connection 452 | * @return a collection of names of trigger groups which were matched and paused 453 | * @throws JobPersistenceException 454 | */ 455 | @Override 456 | public Collection pauseTriggers(GroupMatcher matcher, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 457 | Set pausedTriggerGroups = new HashSet<>(); 458 | if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) { 459 | final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(new TriggerKey("", matcher.getCompareToValue())); 460 | final long addResult = jedis.sadd(redisSchema.pausedTriggerGroupsSet(), triggerGroupSetKey); 461 | if (addResult > 0) { 462 | for (final String trigger : jedis.smembers(triggerGroupSetKey)) { 463 | pauseTrigger(redisSchema.triggerKey(trigger), jedis); 464 | } 465 | pausedTriggerGroups.add(redisSchema.triggerGroup(triggerGroupSetKey)); 466 | } 467 | } else { 468 | Map> triggerGroups = new HashMap<>(); 469 | for (final String triggerGroupSetKey : jedis.smembers(redisSchema.triggerGroupsSet())) { 470 | if (matcher.getCompareWithOperator().evaluate(redisSchema.triggerGroup(triggerGroupSetKey), matcher.getCompareToValue())) { 471 | triggerGroups.put(triggerGroupSetKey, jedis.smembers(triggerGroupSetKey)); 472 | } 473 | } 474 | for (final Map.Entry> entry : triggerGroups.entrySet()) { 475 | if (jedis.sadd(redisSchema.pausedJobGroupsSet(), entry.getKey()) > 0) { 476 | // This trigger group was not paused. Pause it now. 477 | pausedTriggerGroups.add(redisSchema.triggerGroup(entry.getKey())); 478 | for (final String triggerHashKey : entry.getValue()) { 479 | pauseTrigger(redisSchema.triggerKey(triggerHashKey), jedis); 480 | } 481 | } 482 | } 483 | } 484 | return pausedTriggerGroups; 485 | } 486 | 487 | /** 488 | * Pause all of the {@link Job}s in the given group - by pausing all of their 489 | * Triggers. 490 | * 491 | * @param groupMatcher the mather which will determine which job group should be paused 492 | * @param jedis a thread-safe Redis connection 493 | * @return a collection of names of job groups which have been paused 494 | * @throws JobPersistenceException 495 | */ 496 | @Override 497 | public Collection pauseJobs(GroupMatcher groupMatcher, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 498 | Set pausedJobGroups = new HashSet<>(); 499 | if (groupMatcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) { 500 | final String jobGroupSetKey = redisSchema.jobGroupSetKey(new JobKey("", groupMatcher.getCompareToValue())); 501 | if (jedis.sadd(redisSchema.pausedJobGroupsSet(), jobGroupSetKey) > 0) { 502 | pausedJobGroups.add(redisSchema.jobGroup(jobGroupSetKey)); 503 | for (String job : jedis.smembers(jobGroupSetKey)) { 504 | pauseJob(redisSchema.jobKey(job), jedis); 505 | } 506 | } 507 | } else { 508 | Map> jobGroups = new HashMap<>(); 509 | for (final String jobGroupSetKey : jedis.smembers(redisSchema.jobGroupsSet())) { 510 | if (groupMatcher.getCompareWithOperator().evaluate(redisSchema.jobGroup(jobGroupSetKey), groupMatcher.getCompareToValue())) { 511 | jobGroups.put(jobGroupSetKey, jedis.smembers(jobGroupSetKey)); 512 | } 513 | } 514 | for (final Map.Entry> entry : jobGroups.entrySet()) { 515 | if (jedis.sadd(redisSchema.pausedJobGroupsSet(), entry.getKey()) > 0) { 516 | // This job group was not already paused. Pause it now. 517 | pausedJobGroups.add(redisSchema.jobGroup(entry.getKey())); 518 | for (final String jobHashKey : entry.getValue()) { 519 | pauseJob(redisSchema.jobKey(jobHashKey), jedis); 520 | } 521 | } 522 | } 523 | } 524 | return pausedJobGroups; 525 | } 526 | 527 | /** 528 | * Resume (un-pause) a {@link Trigger} 529 | * 530 | * @param triggerKey the key of the trigger to be resumed 531 | * @param jedis a thread-safe Redis connection 532 | */ 533 | @Override 534 | public void resumeTrigger(TriggerKey triggerKey, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 535 | final String triggerHashKey = redisSchema.triggerHashKey(triggerKey); 536 | Boolean exists = jedis.sismember(redisSchema.triggersSet(), triggerHashKey); 537 | Double isPaused = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED), triggerHashKey); 538 | Double isPausedBlocked = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED_BLOCKED), triggerHashKey); 539 | 540 | if (!exists) { 541 | // Trigger does not exist. Nothing to do. 542 | return; 543 | } 544 | if (isPaused == null && isPausedBlocked == null) { 545 | // Trigger is not paused. Nothing to do. 546 | return; 547 | } 548 | OperableTrigger trigger = retrieveTrigger(triggerKey, jedis); 549 | final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey()); 550 | final Date nextFireTime = trigger.getNextFireTime(); 551 | 552 | if (nextFireTime != null) { 553 | if (isBlockedJob(jobHashKey, jedis)) { 554 | setTriggerState(RedisTriggerState.BLOCKED, (double) nextFireTime.getTime(), triggerHashKey, jedis); 555 | } else { 556 | setTriggerState(RedisTriggerState.WAITING, (double) nextFireTime.getTime(), triggerHashKey, jedis); 557 | } 558 | } 559 | applyMisfire(trigger, jedis); 560 | } 561 | 562 | /** 563 | * Resume (un-pause) all of the {@link Trigger}s in the given group. 564 | * 565 | * @param matcher matcher for the trigger groups to be resumed 566 | * @param jedis a thread-safe Redis connection 567 | * @return the names of trigger groups which were resumed 568 | */ 569 | @Override 570 | public Collection resumeTriggers(GroupMatcher matcher, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 571 | Set resumedTriggerGroups = new HashSet<>(); 572 | if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) { 573 | final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(new TriggerKey("", matcher.getCompareToValue())); 574 | jedis.srem(redisSchema.pausedJobGroupsSet(), triggerGroupSetKey); 575 | Set triggerHashKeysResponse = jedis.smembers(triggerGroupSetKey); 576 | for (String triggerHashKey : triggerHashKeysResponse) { 577 | OperableTrigger trigger = retrieveTrigger(redisSchema.triggerKey(triggerHashKey), jedis); 578 | resumeTrigger(trigger.getKey(), jedis); 579 | resumedTriggerGroups.add(trigger.getKey().getGroup()); 580 | } 581 | } else { 582 | for (final String triggerGroupSetKey : jedis.smembers(redisSchema.triggerGroupsSet())) { 583 | if (matcher.getCompareWithOperator().evaluate(redisSchema.triggerGroup(triggerGroupSetKey), matcher.getCompareToValue())) { 584 | resumedTriggerGroups.addAll(resumeTriggers(GroupMatcher.triggerGroupEquals(redisSchema.triggerGroup(triggerGroupSetKey)), jedis)); 585 | } 586 | } 587 | } 588 | return resumedTriggerGroups; 589 | } 590 | 591 | /** 592 | * Resume (un-pause) all of the {@link Job}s in the given group. 593 | * 594 | * @param matcher the matcher with which to compare job group names 595 | * @param jedis a thread-safe Redis connection 596 | * @return the set of job groups which were matched and resumed 597 | */ 598 | @Override 599 | public Collection resumeJobs(GroupMatcher matcher, JedisClusterCommandsWrapper jedis) throws JobPersistenceException { 600 | Set resumedJobGroups = new HashSet<>(); 601 | if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) { 602 | final String jobGroupSetKey = redisSchema.jobGroupSetKey(new JobKey("", matcher.getCompareToValue())); 603 | Long unpauseResponse = jedis.srem(redisSchema.pausedJobGroupsSet(), jobGroupSetKey); 604 | Set jobsResponse = jedis.smembers(jobGroupSetKey); 605 | if (unpauseResponse > 0) { 606 | resumedJobGroups.add(redisSchema.jobGroup(jobGroupSetKey)); 607 | } 608 | for (String job : jobsResponse) { 609 | resumeJob(redisSchema.jobKey(job), jedis); 610 | } 611 | } else { 612 | for (final String jobGroupSetKey : jedis.smembers(redisSchema.jobGroupsSet())) { 613 | if (matcher.getCompareWithOperator().evaluate(redisSchema.jobGroup(jobGroupSetKey), matcher.getCompareToValue())) { 614 | resumedJobGroups.addAll(resumeJobs(GroupMatcher.jobGroupEquals(redisSchema.jobGroup(jobGroupSetKey)), jedis)); 615 | } 616 | } 617 | } 618 | return resumedJobGroups; 619 | } 620 | 621 | /** 622 | * Inform the JobStore that the scheduler is now firing the 623 | * given Trigger (executing its associated Job), 624 | * that it had previously acquired (reserved). 625 | * 626 | * @param triggers a list of triggers 627 | * @param jedis a thread-safe Redis connection 628 | * @return may return null if all the triggers or their calendars no longer exist, or 629 | * if the trigger was not successfully put into the 'executing' 630 | * state. Preference is to return an empty list if none of the triggers 631 | * could be fired. 632 | */ 633 | @Override 634 | public List triggersFired(List triggers, JedisClusterCommandsWrapper jedis) throws JobPersistenceException, ClassNotFoundException { 635 | List results = new ArrayList<>(); 636 | for (OperableTrigger trigger : triggers) { 637 | final String triggerHashKey = redisSchema.triggerHashKey(trigger.getKey()); 638 | logger.debug(String.format("Trigger %s fired.", triggerHashKey)); 639 | Boolean triggerExistsResponse = jedis.exists(triggerHashKey); 640 | Double triggerAcquiredResponse = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.ACQUIRED), triggerHashKey); 641 | if (!triggerExistsResponse || triggerAcquiredResponse == null) { 642 | // the trigger does not exist or the trigger is not acquired 643 | if (!triggerExistsResponse) { 644 | logger.debug(String.format("Trigger %s does not exist.", triggerHashKey)); 645 | } else { 646 | logger.debug(String.format("Trigger %s was not acquired.", triggerHashKey)); 647 | } 648 | continue; 649 | } 650 | Calendar calendar = null; 651 | final String calendarName = trigger.getCalendarName(); 652 | if (calendarName != null) { 653 | calendar = retrieveCalendar(calendarName, jedis); 654 | if (calendar == null) { 655 | continue; 656 | } 657 | } 658 | 659 | final Date previousFireTime = trigger.getPreviousFireTime(); 660 | trigger.triggered(calendar); 661 | 662 | JobDetail job = retrieveJob(trigger.getJobKey(), jedis); 663 | TriggerFiredBundle triggerFiredBundle = new TriggerFiredBundle(job, trigger, calendar, false, new Date(), previousFireTime, previousFireTime, trigger.getNextFireTime()); 664 | 665 | // handling jobs for which concurrent execution is disallowed 666 | if (isJobConcurrentExecutionDisallowed(job.getJobClass())) { 667 | final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey()); 668 | final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(job.getKey()); 669 | for (String nonConcurrentTriggerHashKey : jedis.smembers(jobTriggerSetKey)) { 670 | Double score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.WAITING), nonConcurrentTriggerHashKey); 671 | if (score != null) { 672 | setTriggerState(RedisTriggerState.BLOCKED, score, nonConcurrentTriggerHashKey, jedis); 673 | // setting trigger state removes locks, so re-lock 674 | lockTrigger(redisSchema.triggerKey(nonConcurrentTriggerHashKey), jedis); 675 | } else { 676 | score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED), nonConcurrentTriggerHashKey); 677 | if (score != null) { 678 | setTriggerState(RedisTriggerState.PAUSED_BLOCKED, score, nonConcurrentTriggerHashKey, jedis); 679 | // setting trigger state removes locks, so re-lock 680 | lockTrigger(redisSchema.triggerKey(nonConcurrentTriggerHashKey), jedis); 681 | } 682 | } 683 | } 684 | jedis.set(redisSchema.jobBlockedKey(job.getKey()), schedulerInstanceId); 685 | jedis.sadd(redisSchema.blockedJobsSet(), jobHashKey); 686 | } 687 | 688 | // release the fired trigger 689 | if (trigger.getNextFireTime() != null) { 690 | final long nextFireTime = trigger.getNextFireTime().getTime(); 691 | jedis.hset(triggerHashKey, TRIGGER_NEXT_FIRE_TIME, Long.toString(nextFireTime)); 692 | logger.debug(String.format("Releasing trigger %s with next fire time %s. Setting state to WAITING.", triggerHashKey, nextFireTime)); 693 | setTriggerState(RedisTriggerState.WAITING, (double) nextFireTime, triggerHashKey, jedis); 694 | } else { 695 | jedis.hset(triggerHashKey, TRIGGER_NEXT_FIRE_TIME, ""); 696 | unsetTriggerState(triggerHashKey, jedis); 697 | } 698 | jedis.hset(triggerHashKey, TRIGGER_PREVIOUS_FIRE_TIME, Long.toString(System.currentTimeMillis())); 699 | 700 | results.add(new TriggerFiredResult(triggerFiredBundle)); 701 | } 702 | return results; 703 | } 704 | 705 | /** 706 | * Inform the JobStore that the scheduler has completed the 707 | * firing of the given Trigger (and the execution of its 708 | * associated Job completed, threw an exception, or was vetoed), 709 | * and that the {@link JobDataMap} 710 | * in the given JobDetail should be updated if the Job 711 | * is stateful. 712 | * 713 | * @param trigger the trigger which was completed 714 | * @param jobDetail the job which was completed 715 | * @param triggerInstCode the status of the completed job 716 | * @param jedis a thread-safe Redis connection 717 | */ 718 | @Override 719 | public void triggeredJobComplete(OperableTrigger trigger, JobDetail jobDetail, Trigger.CompletedExecutionInstruction triggerInstCode, JedisClusterCommandsWrapper jedis) throws JobPersistenceException, ClassNotFoundException { 720 | final String jobHashKey = redisSchema.jobHashKey(jobDetail.getKey()); 721 | final String jobDataMapHashKey = redisSchema.jobDataMapHashKey(jobDetail.getKey()); 722 | final String triggerHashKey = redisSchema.triggerHashKey(trigger.getKey()); 723 | logger.debug(String.format("Job %s completed.", jobHashKey)); 724 | if (jedis.exists(jobHashKey)) { 725 | // job was not deleted during execution 726 | if (isPersistJobDataAfterExecution(jobDetail.getJobClass())) { 727 | // update the job data map 728 | JobDataMap jobDataMap = jobDetail.getJobDataMap(); 729 | jedis.del(jobDataMapHashKey); 730 | if (jobDataMap != null && !jobDataMap.isEmpty()) { 731 | jedis.hmset(jobDataMapHashKey, getStringDataMap(jobDataMap)); 732 | } 733 | } 734 | if (isJobConcurrentExecutionDisallowed(jobDetail.getJobClass())) { 735 | // unblock the job 736 | jedis.srem(redisSchema.blockedJobsSet(), jobHashKey); 737 | jedis.del(redisSchema.jobBlockedKey(jobDetail.getKey())); 738 | 739 | final String jobTriggersSetKey = redisSchema.jobTriggersSetKey(jobDetail.getKey()); 740 | for (String nonConcurrentTriggerHashKey : jedis.smembers(jobTriggersSetKey)) { 741 | Double score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.BLOCKED), nonConcurrentTriggerHashKey); 742 | if (score != null) { 743 | setTriggerState(RedisTriggerState.WAITING, score, nonConcurrentTriggerHashKey, jedis); 744 | } else { 745 | score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED_BLOCKED), nonConcurrentTriggerHashKey); 746 | if (score != null) { 747 | setTriggerState(RedisTriggerState.PAUSED, score, nonConcurrentTriggerHashKey, jedis); 748 | } 749 | } 750 | } 751 | signaler.signalSchedulingChange(0L); 752 | } 753 | } else { 754 | // unblock the job, even if it has been deleted 755 | jedis.srem(redisSchema.blockedJobsSet(), jobHashKey); 756 | } 757 | 758 | if (jedis.exists(triggerHashKey)) { 759 | // trigger was not deleted during job execution 760 | if (triggerInstCode == Trigger.CompletedExecutionInstruction.DELETE_TRIGGER) { 761 | if (trigger.getNextFireTime() == null) { 762 | // double-check for possible reschedule within job execution, which would cancel the need to delete 763 | if (isNullOrEmpty(jedis.hget(triggerHashKey, TRIGGER_NEXT_FIRE_TIME))) { 764 | removeTrigger(trigger.getKey(), jedis); 765 | } 766 | } else { 767 | removeTrigger(trigger.getKey(), jedis); 768 | signaler.signalSchedulingChange(0L); 769 | } 770 | } else if (triggerInstCode == Trigger.CompletedExecutionInstruction.SET_TRIGGER_COMPLETE) { 771 | setTriggerState(RedisTriggerState.COMPLETED, (double) System.currentTimeMillis(), triggerHashKey, jedis); 772 | signaler.signalSchedulingChange(0L); 773 | } else if (triggerInstCode == Trigger.CompletedExecutionInstruction.SET_TRIGGER_ERROR) { 774 | logger.debug(String.format("Trigger %s set to ERROR state.", triggerHashKey)); 775 | final double score = trigger.getNextFireTime() != null ? (double) trigger.getNextFireTime().getTime() : 0; 776 | setTriggerState(RedisTriggerState.ERROR, score, triggerHashKey, jedis); 777 | signaler.signalSchedulingChange(0L); 778 | } else if (triggerInstCode == Trigger.CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR) { 779 | final String jobTriggersSetKey = redisSchema.jobTriggersSetKey(jobDetail.getKey()); 780 | for (String errorTriggerHashKey : jedis.smembers(jobTriggersSetKey)) { 781 | final String nextFireTime = jedis.hget(errorTriggerHashKey, TRIGGER_NEXT_FIRE_TIME); 782 | final double score = isNullOrEmpty(nextFireTime) ? 0 : Double.parseDouble(nextFireTime); 783 | setTriggerState(RedisTriggerState.ERROR, score, errorTriggerHashKey, jedis); 784 | } 785 | signaler.signalSchedulingChange(0L); 786 | } else if (triggerInstCode == Trigger.CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_COMPLETE) { 787 | final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(jobDetail.getKey()); 788 | for (String completedTriggerHashKey : jedis.smembers(jobTriggerSetKey)) { 789 | setTriggerState(RedisTriggerState.COMPLETED, (double) System.currentTimeMillis(), completedTriggerHashKey, jedis); 790 | } 791 | signaler.signalSchedulingChange(0L); 792 | } 793 | } 794 | } 795 | } 796 | -------------------------------------------------------------------------------- /src/main/java/net/joelinn/quartz/jobstore/RedisJobStoreSchema.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.jobstore; 2 | 3 | import org.quartz.JobKey; 4 | import org.quartz.TriggerKey; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | /** 10 | * Joe Linn 11 | * 7/14/2014 12 | */ 13 | public class RedisJobStoreSchema { 14 | protected static final String DEFAULT_DELIMITER = ":"; 15 | protected static final String JOBS_SET = "jobs"; 16 | protected static final String JOB_GROUPS_SET = "job_groups"; 17 | protected static final String LOCK = "lock"; 18 | 19 | protected final String prefix; 20 | 21 | protected final String delimiter; 22 | 23 | public RedisJobStoreSchema(){ 24 | this(""); 25 | } 26 | 27 | /** 28 | * @param prefix the prefix to be prepended to all redis keys 29 | */ 30 | public RedisJobStoreSchema(String prefix){ 31 | this(prefix, DEFAULT_DELIMITER); 32 | } 33 | 34 | /** 35 | * @param prefix the prefix to be prepended to all redis keys 36 | * @param delimiter the delimiter to be used to separate key segments 37 | */ 38 | public RedisJobStoreSchema(String prefix, String delimiter) { 39 | this.prefix = prefix; 40 | this.delimiter = delimiter; 41 | } 42 | 43 | /** 44 | * 45 | * @return the redis key used for locking 46 | */ 47 | public String lockKey(){ 48 | return addPrefix(LOCK); 49 | } 50 | 51 | /** 52 | * @return the redis key for the set containing all job keys 53 | */ 54 | public String jobsSet(){ 55 | return addPrefix(JOBS_SET); 56 | } 57 | 58 | /** 59 | * @return the redis key for the set containing all job group keys 60 | */ 61 | public String jobGroupsSet(){ 62 | return addPrefix(JOB_GROUPS_SET); 63 | } 64 | 65 | /** 66 | * 67 | * @param jobKey 68 | * @return the redis key associated with the given {@link org.quartz.JobKey} 69 | */ 70 | public String jobHashKey(final JobKey jobKey){ 71 | return addPrefix("job" + delimiter + jobKey.getGroup() + delimiter + jobKey.getName()); 72 | } 73 | 74 | /** 75 | * 76 | * @param jobKey 77 | * @return the redis key associated with the job data for the given {@link org.quartz.JobKey} 78 | */ 79 | public String jobDataMapHashKey(final JobKey jobKey){ 80 | return addPrefix("job_data_map" + delimiter + jobKey.getGroup() + delimiter + jobKey.getName()); 81 | } 82 | 83 | /** 84 | * 85 | * @param jobKey 86 | * @return the key associated with the group set for the given {@link org.quartz.JobKey} 87 | */ 88 | public String jobGroupSetKey(final JobKey jobKey){ 89 | return addPrefix("job_group" + delimiter + jobKey.getGroup()); 90 | } 91 | 92 | /** 93 | * 94 | * @param jobHashKey the hash key for a job 95 | * @return the {@link org.quartz.JobKey} object describing the job 96 | */ 97 | public JobKey jobKey(final String jobHashKey){ 98 | final List hashParts = split(jobHashKey); 99 | return new JobKey(hashParts.get(2), hashParts.get(1)); 100 | } 101 | 102 | /** 103 | * 104 | * @param jobGroupSetKey the redis key for a job group set 105 | * @return the name of the job group 106 | */ 107 | public String jobGroup(final String jobGroupSetKey){ 108 | return split(jobGroupSetKey).get(1); 109 | } 110 | 111 | /** 112 | * @param jobKey the job key for which to get a trigger set key 113 | * @return the key associated with the set of triggers for the given {@link org.quartz.JobKey} 114 | */ 115 | public String jobTriggersSetKey(final JobKey jobKey){ 116 | return addPrefix("job_triggers" + delimiter + jobKey.getGroup() + delimiter + jobKey.getName()); 117 | } 118 | 119 | /** 120 | * @return the key associated with the set of blocked jobs 121 | */ 122 | public String blockedJobsSet(){ 123 | return addPrefix("blocked_jobs"); 124 | } 125 | 126 | /** 127 | * 128 | * @param triggerKey a trigger key 129 | * @return the redis key associated with the given {@link org.quartz.TriggerKey} 130 | */ 131 | public String triggerHashKey(final TriggerKey triggerKey){ 132 | return addPrefix("trigger" + delimiter + triggerKey.getGroup() + delimiter + triggerKey.getName()); 133 | } 134 | 135 | /** 136 | * 137 | * @param triggerKey 138 | * @return the redis key associated with the trigger data for the given {@link org.quartz.TriggerKey} 139 | */ 140 | public String triggerDataMapHashKey(final TriggerKey triggerKey){ 141 | return addPrefix("trigger_data_map" + delimiter + triggerKey.getGroup() + delimiter + triggerKey.getName()); 142 | } 143 | 144 | /** 145 | * 146 | * @param triggerHashKey the hash key for a trigger 147 | * @return the {@link org.quartz.TriggerKey} object describing the desired trigger 148 | */ 149 | public TriggerKey triggerKey(final String triggerHashKey){ 150 | final List hashParts = split(triggerHashKey); 151 | return new TriggerKey(hashParts.get(2), hashParts.get(1)); 152 | } 153 | 154 | /** 155 | * 156 | * @param triggerGroupSetKey the redis key for a trigger group set 157 | * @return the name of the trigger group represented by the given redis key 158 | */ 159 | public String triggerGroup(final String triggerGroupSetKey){ 160 | return split(triggerGroupSetKey).get(1); 161 | } 162 | 163 | /** 164 | * @param triggerKey a trigger key 165 | * @return the redis key associated with the group of the given {@link org.quartz.TriggerKey} 166 | */ 167 | public String triggerGroupSetKey(final TriggerKey triggerKey){ 168 | return addPrefix("trigger_group" + delimiter + triggerKey.getGroup()); 169 | } 170 | 171 | /** 172 | * @return the key of the set containing all trigger keys 173 | */ 174 | public String triggersSet(){ 175 | return addPrefix("triggers"); 176 | } 177 | 178 | /** 179 | * @return the key of the set containing all trigger group keys 180 | */ 181 | public String triggerGroupsSet(){ 182 | return addPrefix("trigger_groups"); 183 | } 184 | 185 | /** 186 | * @return the key of the set containing paused trigger group keys 187 | */ 188 | public String pausedTriggerGroupsSet(){ 189 | return addPrefix("paused_trigger_groups"); 190 | } 191 | 192 | /** 193 | * @param state a {@link net.joelinn.quartz.jobstore.RedisTriggerState} 194 | * @return the key of a set containing the keys of triggers which are in the given state 195 | */ 196 | public String triggerStateKey(final RedisTriggerState state){ 197 | return addPrefix(state.getKey()); 198 | } 199 | 200 | /** 201 | * 202 | * @param triggerKey the key of the trigger for which to retrieve a lock key 203 | * @return the redis key for the lock state of the given trigger 204 | */ 205 | public String triggerLockKey(final TriggerKey triggerKey){ 206 | return addPrefix("trigger_lock" + delimiter + triggerKey.getGroup() + delimiter + triggerKey.getName()); 207 | } 208 | 209 | /** 210 | * 211 | * @param jobKey the key of the job for which to retrieve a block key 212 | * @return the redis key for the blocked state of the given job 213 | */ 214 | public String jobBlockedKey(final JobKey jobKey){ 215 | return addPrefix("job_blocked" + delimiter + jobKey.getGroup() + delimiter + jobKey.getName()); 216 | } 217 | 218 | /** 219 | * @return the key which holds the time at which triggers were last released 220 | */ 221 | public String lastTriggerReleaseTime(){ 222 | return addPrefix("last_triggers_release_time"); 223 | } 224 | 225 | /** 226 | * @return the key which holds a hash of scheduler instance ids to last active time 227 | */ 228 | public String lastInstanceActiveTime(){ 229 | return addPrefix("last_instance_active_time"); 230 | } 231 | 232 | /** 233 | * @return the key of the set containing paused job groups 234 | */ 235 | public String pausedJobGroupsSet(){ 236 | return addPrefix("paused_job_groups"); 237 | } 238 | 239 | /** 240 | * @param calendarName the name of the calendar for which to retrieve a key 241 | * @return the redis key for the set containing trigger keys for the given calendar name 242 | */ 243 | public String calendarTriggersSetKey(final String calendarName){ 244 | return addPrefix("calendar_triggers" + delimiter + calendarName); 245 | } 246 | 247 | /** 248 | * @param calendarName the name of the calendar for which to retrieve a key 249 | * @return the redis key for the calendar with the given name 250 | */ 251 | public String calendarHashKey(final String calendarName){ 252 | return addPrefix("calendar" + delimiter + calendarName); 253 | } 254 | 255 | /** 256 | * 257 | * @param calendarHashKey the redis key for a calendar 258 | * @return the name of the calendar represented by the given key 259 | */ 260 | public String calendarName(final String calendarHashKey){ 261 | return split(calendarHashKey).get(1); 262 | } 263 | 264 | /** 265 | * @return the key of the set containing all calendar keys 266 | */ 267 | public String calendarsSet(){ 268 | return addPrefix("calendars"); 269 | } 270 | 271 | /** 272 | * Add the configured prefix string to the given key 273 | * @param key the key to which the prefix should be prepended 274 | * @return a prefixed key 275 | */ 276 | protected String addPrefix(String key){ 277 | return prefix + key; 278 | } 279 | 280 | /** 281 | * Split a string on the configured delimiter 282 | * @param string the string to split 283 | * @return a list comprised of the split parts of the given string 284 | */ 285 | protected List split(final String string){ 286 | if (null!=prefix){ 287 | //remove prefix before split 288 | return Arrays.asList(string.substring(prefix.length()).split(delimiter)); 289 | }else{ 290 | return Arrays.asList(string.split(delimiter)); 291 | } 292 | 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/main/java/net/joelinn/quartz/jobstore/RedisTriggerState.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.jobstore; 2 | 3 | import org.quartz.Trigger; 4 | 5 | /** 6 | * Joe Linn 7 | * 7/15/2014 8 | */ 9 | public enum RedisTriggerState { 10 | WAITING("waiting_triggers", Trigger.TriggerState.NORMAL), 11 | PAUSED("paused_triggers", Trigger.TriggerState.PAUSED), 12 | BLOCKED("blocked_triggers", Trigger.TriggerState.BLOCKED), 13 | PAUSED_BLOCKED("paused_blocked_triggers", Trigger.TriggerState.PAUSED), 14 | ACQUIRED("acquired_triggers", Trigger.TriggerState.NORMAL), 15 | COMPLETED("completed_triggers", Trigger.TriggerState.COMPLETE), 16 | ERROR("error_triggers", Trigger.TriggerState.ERROR); 17 | 18 | private final String key; 19 | 20 | private final Trigger.TriggerState triggerState; 21 | 22 | RedisTriggerState(String key, Trigger.TriggerState triggerState) { 23 | this.key = key; 24 | this.triggerState = triggerState; 25 | } 26 | 27 | public String getKey() { 28 | return key; 29 | } 30 | 31 | public Trigger.TriggerState getTriggerState() { 32 | return triggerState; 33 | } 34 | 35 | public static RedisTriggerState toState(String key){ 36 | for (RedisTriggerState state : RedisTriggerState.values()) { 37 | if(state.getKey().equals(key)){ 38 | return state; 39 | } 40 | } 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/net/joelinn/quartz/jobstore/jedis/JedisClusterCommandsWrapper.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.jobstore.jedis; 2 | 3 | import redis.clients.jedis.*; 4 | import redis.clients.jedis.commands.JedisCommands; 5 | import redis.clients.jedis.params.GeoRadiusParam; 6 | import redis.clients.jedis.params.SetParams; 7 | import redis.clients.jedis.params.ZAddParams; 8 | import redis.clients.jedis.params.ZIncrByParams; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Set; 13 | 14 | /** 15 | * Unfortunately, {@link JedisCluster} does not implement the {@link JedisCommands} interface, even though the vast 16 | * majority of its method signatures line up. This class works around that issue. Hopefully future versions of Jedis 17 | * will render this unnecessary. 18 | * @author Joe Linn 19 | * 7/2/2019 20 | */ 21 | public class JedisClusterCommandsWrapper implements JedisCommands { 22 | private final JedisCluster cluster; 23 | 24 | public JedisClusterCommandsWrapper(JedisCluster cluster) { 25 | this.cluster = cluster; 26 | } 27 | 28 | 29 | @Override 30 | public String set(String s, String s1) { 31 | return cluster.set(s, s1); 32 | } 33 | 34 | @Override 35 | public String set(String s, String s1, SetParams setParams) { 36 | return cluster.set(s, s1, setParams); 37 | } 38 | 39 | @Override 40 | public String get(String s) { 41 | return cluster.get(s); 42 | } 43 | 44 | @Override 45 | public Boolean exists(String s) { 46 | return cluster.exists(s); 47 | } 48 | 49 | @Override 50 | public Long persist(String s) { 51 | return cluster.persist(s); 52 | } 53 | 54 | @Override 55 | public String type(String s) { 56 | return cluster.type(s); 57 | } 58 | 59 | @Override 60 | public byte[] dump(String s) { 61 | return cluster.dump(s); 62 | } 63 | 64 | @Override 65 | public String restore(String s, int i, byte[] bytes) { 66 | return cluster.restore(s, i, bytes); 67 | } 68 | 69 | @Override 70 | public String restoreReplace(String s, int i, byte[] bytes) { 71 | return null; 72 | } 73 | 74 | @Override 75 | public Long expire(String s, int i) { 76 | return cluster.expire(s, i); 77 | } 78 | 79 | @Override 80 | public Long pexpire(String s, long l) { 81 | return cluster.pexpire(s, l); 82 | } 83 | 84 | @Override 85 | public Long expireAt(String s, long l) { 86 | return cluster.expireAt(s, l); 87 | } 88 | 89 | @Override 90 | public Long pexpireAt(String s, long l) { 91 | return cluster.pexpireAt(s, l); 92 | } 93 | 94 | @Override 95 | public Long ttl(String s) { 96 | return cluster.ttl(s); 97 | } 98 | 99 | @Override 100 | public Long pttl(String s) { 101 | return cluster.pttl(s); 102 | } 103 | 104 | @Override 105 | public Long touch(String s) { 106 | return cluster.touch(s); 107 | } 108 | 109 | @Override 110 | public Boolean setbit(String s, long l, boolean b) { 111 | return cluster.setbit(s, l, b); 112 | } 113 | 114 | @Override 115 | public Boolean setbit(String s, long l, String s1) { 116 | return cluster.setbit(s, l, s1); 117 | } 118 | 119 | @Override 120 | public Boolean getbit(String s, long l) { 121 | return cluster.getbit(s, l); 122 | } 123 | 124 | @Override 125 | public Long setrange(String s, long l, String s1) { 126 | return cluster.setrange(s, l, s1); 127 | } 128 | 129 | @Override 130 | public String getrange(String s, long l, long l1) { 131 | return cluster.getrange(s, l, l1); 132 | } 133 | 134 | @Override 135 | public String getSet(String s, String s1) { 136 | return cluster.getSet(s, s1); 137 | } 138 | 139 | @Override 140 | public Long setnx(String s, String s1) { 141 | return cluster.setnx(s, s1); 142 | } 143 | 144 | @Override 145 | public String setex(String s, int i, String s1) { 146 | return cluster.setex(s, i, s1); 147 | } 148 | 149 | @Override 150 | public String psetex(String s, long l, String s1) { 151 | return cluster.psetex(s, l, s1); 152 | } 153 | 154 | @Override 155 | public Long decrBy(String s, long l) { 156 | return cluster.decrBy(s, l); 157 | } 158 | 159 | @Override 160 | public Long decr(String s) { 161 | return cluster.decr(s); 162 | } 163 | 164 | @Override 165 | public Long incrBy(String s, long l) { 166 | return cluster.incrBy(s, l); 167 | } 168 | 169 | @Override 170 | public Double incrByFloat(String s, double v) { 171 | return cluster.incrByFloat(s, v); 172 | } 173 | 174 | @Override 175 | public Long incr(String s) { 176 | return cluster.incr(s); 177 | } 178 | 179 | @Override 180 | public Long append(String s, String s1) { 181 | return cluster.append(s, s1); 182 | } 183 | 184 | @Override 185 | public String substr(String s, int i, int i1) { 186 | return cluster.substr(s, i, i1); 187 | } 188 | 189 | @Override 190 | public Long hset(String s, String s1, String s2) { 191 | return cluster.hset(s, s1, s2); 192 | } 193 | 194 | @Override 195 | public Long hset(String s, Map map) { 196 | return cluster.hset(s, map); 197 | } 198 | 199 | @Override 200 | public String hget(String s, String s1) { 201 | return cluster.hget(s, s1); 202 | } 203 | 204 | @Override 205 | public Long hsetnx(String s, String s1, String s2) { 206 | return cluster.hsetnx(s, s1, s2); 207 | } 208 | 209 | @Override 210 | public String hmset(String s, Map map) { 211 | return cluster.hmset(s, map); 212 | } 213 | 214 | @Override 215 | public List hmget(String s, String... strings) { 216 | return cluster.hmget(s, strings); 217 | } 218 | 219 | @Override 220 | public Long hincrBy(String s, String s1, long l) { 221 | return cluster.hincrBy(s, s1, l); 222 | } 223 | 224 | @Override 225 | public Double hincrByFloat(String s, String s1, double v) { 226 | return cluster.hincrByFloat(s.getBytes(), s1.getBytes(), v); 227 | } 228 | 229 | @Override 230 | public Boolean hexists(String s, String s1) { 231 | return cluster.hexists(s, s1); 232 | } 233 | 234 | @Override 235 | public Long hdel(String s, String... strings) { 236 | return cluster.hdel(s, strings); 237 | } 238 | 239 | @Override 240 | public Long hlen(String s) { 241 | return cluster.hlen(s); 242 | } 243 | 244 | @Override 245 | public Set hkeys(String s) { 246 | return cluster.hkeys(s); 247 | } 248 | 249 | @Override 250 | public List hvals(String s) { 251 | return cluster.hvals(s); 252 | } 253 | 254 | @Override 255 | public Map hgetAll(String s) { 256 | return cluster.hgetAll(s); 257 | } 258 | 259 | @Override 260 | public Long rpush(String s, String... strings) { 261 | return cluster.rpush(s, strings); 262 | } 263 | 264 | @Override 265 | public Long lpush(String s, String... strings) { 266 | return cluster.lpush(s, strings); 267 | } 268 | 269 | @Override 270 | public Long llen(String s) { 271 | return cluster.llen(s); 272 | } 273 | 274 | @Override 275 | public List lrange(String s, long l, long l1) { 276 | return cluster.lrange(s, l, l1); 277 | } 278 | 279 | @Override 280 | public String ltrim(String s, long l, long l1) { 281 | return cluster.ltrim(s, l, l1); 282 | } 283 | 284 | @Override 285 | public String lindex(String s, long l) { 286 | return cluster.lindex(s, l); 287 | } 288 | 289 | @Override 290 | public String lset(String s, long l, String s1) { 291 | return cluster.lset(s, l, s1); 292 | } 293 | 294 | @Override 295 | public Long lrem(String s, long l, String s1) { 296 | return cluster.lrem(s, l, s1); 297 | } 298 | 299 | @Override 300 | public String lpop(String s) { 301 | return cluster.lpop(s); 302 | } 303 | 304 | @Override 305 | public String rpop(String s) { 306 | return cluster.rpop(s); 307 | } 308 | 309 | @Override 310 | public Long sadd(String s, String... strings) { 311 | return cluster.sadd(s, strings); 312 | } 313 | 314 | @Override 315 | public Set smembers(String s) { 316 | return cluster.smembers(s); 317 | } 318 | 319 | @Override 320 | public Long srem(String s, String... strings) { 321 | return cluster.srem(s, strings); 322 | } 323 | 324 | @Override 325 | public String spop(String s) { 326 | return cluster.spop(s); 327 | } 328 | 329 | @Override 330 | public Set spop(String s, long l) { 331 | return cluster.spop(s, l); 332 | } 333 | 334 | @Override 335 | public Long scard(String s) { 336 | return cluster.scard(s); 337 | } 338 | 339 | @Override 340 | public Boolean sismember(String s, String s1) { 341 | return cluster.sismember(s, s1); 342 | } 343 | 344 | @Override 345 | public String srandmember(String s) { 346 | return cluster.srandmember(s); 347 | } 348 | 349 | @Override 350 | public List srandmember(String s, int i) { 351 | return cluster.srandmember(s, i); 352 | } 353 | 354 | @Override 355 | public Long strlen(String s) { 356 | return cluster.strlen(s); 357 | } 358 | 359 | @Override 360 | public Long zadd(String s, double v, String s1) { 361 | return cluster.zadd(s, v, s1); 362 | } 363 | 364 | @Override 365 | public Long zadd(String s, double v, String s1, ZAddParams zAddParams) { 366 | return cluster.zadd(s, v, s1, zAddParams); 367 | } 368 | 369 | @Override 370 | public Long zadd(String s, Map map) { 371 | return cluster.zadd(s, map); 372 | } 373 | 374 | @Override 375 | public Long zadd(String s, Map map, ZAddParams zAddParams) { 376 | return cluster.zadd(s, map, zAddParams); 377 | } 378 | 379 | @Override 380 | public Set zrange(String s, long l, long l1) { 381 | return cluster.zrange(s, l, l1); 382 | } 383 | 384 | @Override 385 | public Long zrem(String s, String... strings) { 386 | return cluster.zrem(s, strings); 387 | } 388 | 389 | @Override 390 | public Double zincrby(String s, double v, String s1) { 391 | return cluster.zincrby(s, v, s1); 392 | } 393 | 394 | @Override 395 | public Double zincrby(String s, double v, String s1, ZIncrByParams zIncrByParams) { 396 | return cluster.zincrby(s, v, s1, zIncrByParams); 397 | } 398 | 399 | @Override 400 | public Long zrank(String s, String s1) { 401 | return cluster.zrank(s, s1); 402 | } 403 | 404 | @Override 405 | public Long zrevrank(String s, String s1) { 406 | return cluster.zrevrank(s, s1); 407 | } 408 | 409 | @Override 410 | public Set zrevrange(String s, long l, long l1) { 411 | return cluster.zrevrange(s, l, l1); 412 | } 413 | 414 | @Override 415 | public Set zrangeWithScores(String s, long l, long l1) { 416 | return cluster.zrangeWithScores(s, l, l1); 417 | } 418 | 419 | @Override 420 | public Set zrevrangeWithScores(String s, long l, long l1) { 421 | return cluster.zrevrangeWithScores(s, l, l1); 422 | } 423 | 424 | @Override 425 | public Long zcard(String s) { 426 | return cluster.zcard(s); 427 | } 428 | 429 | @Override 430 | public Double zscore(String s, String s1) { 431 | return cluster.zscore(s, s1); 432 | } 433 | 434 | @Override 435 | public List sort(String s) { 436 | return cluster.sort(s); 437 | } 438 | 439 | @Override 440 | public List sort(String s, SortingParams sortingParams) { 441 | return cluster.sort(s, sortingParams); 442 | } 443 | 444 | @Override 445 | public Long zcount(String s, double v, double v1) { 446 | return cluster.zcount(s, v, v1); 447 | } 448 | 449 | @Override 450 | public Long zcount(String s, String s1, String s2) { 451 | return cluster.zcount(s, s1, s1); 452 | } 453 | 454 | @Override 455 | public Set zrangeByScore(String s, double v, double v1) { 456 | return cluster.zrangeByScore(s, v, v1); 457 | } 458 | 459 | @Override 460 | public Set zrangeByScore(String s, String s1, String s2) { 461 | return cluster.zrangeByScore(s, s1, s2); 462 | } 463 | 464 | @Override 465 | public Set zrevrangeByScore(String s, double v, double v1) { 466 | return cluster.zrevrangeByScore(s, v, v1); 467 | } 468 | 469 | @Override 470 | public Set zrangeByScore(String s, double v, double v1, int i, int i1) { 471 | return cluster.zrangeByScore(s, v, v1, i, i1); 472 | } 473 | 474 | @Override 475 | public Set zrevrangeByScore(String s, String s1, String s2) { 476 | return cluster.zrevrangeByScore(s, s1, s2); 477 | } 478 | 479 | @Override 480 | public Set zrangeByScore(String s, String s1, String s2, int i, int i1) { 481 | return cluster.zrangeByScore(s, s1, s2, i, i1); 482 | } 483 | 484 | @Override 485 | public Set zrevrangeByScore(String s, double v, double v1, int i, int i1) { 486 | return cluster.zrevrangeByScore(s, v, v1, i, i1); 487 | } 488 | 489 | @Override 490 | public Set zrangeByScoreWithScores(String s, double v, double v1) { 491 | return cluster.zrangeByScoreWithScores(s, v, v1); 492 | } 493 | 494 | @Override 495 | public Set zrevrangeByScoreWithScores(String s, double v, double v1) { 496 | return cluster.zrevrangeByScoreWithScores(s, v, v1); 497 | } 498 | 499 | @Override 500 | public Set zrangeByScoreWithScores(String s, double v, double v1, int i, int i1) { 501 | return cluster.zrangeByScoreWithScores(s, v, v1, i, i1); 502 | } 503 | 504 | @Override 505 | public Set zrevrangeByScore(String s, String s1, String s2, int i, int i1) { 506 | return cluster.zrevrangeByScore(s, s1, s2, i, i1); 507 | } 508 | 509 | @Override 510 | public Set zrangeByScoreWithScores(String s, String s1, String s2) { 511 | return cluster.zrangeByScoreWithScores(s, s1, s2); 512 | } 513 | 514 | @Override 515 | public Set zrevrangeByScoreWithScores(String s, String s1, String s2) { 516 | return cluster.zrevrangeByScoreWithScores(s, s1, s2); 517 | } 518 | 519 | @Override 520 | public Set zrangeByScoreWithScores(String s, String s1, String s2, int i, int i1) { 521 | return cluster.zrangeByScoreWithScores(s, s1, s2, i , i1); 522 | } 523 | 524 | @Override 525 | public Set zrevrangeByScoreWithScores(String s, double v, double v1, int i, int i1) { 526 | return cluster.zrevrangeByScoreWithScores(s, v, v1, i, i1); 527 | } 528 | 529 | @Override 530 | public Set zrevrangeByScoreWithScores(String s, String s1, String s2, int i, int i1) { 531 | return cluster.zrevrangeByScoreWithScores(s, s1, s2, i, i1); 532 | } 533 | 534 | @Override 535 | public Long zremrangeByRank(String s, long l, long l1) { 536 | return cluster.zremrangeByRank(s, l, l1); 537 | } 538 | 539 | @Override 540 | public Long zremrangeByScore(String s, double v, double v1) { 541 | return cluster.zremrangeByScore(s, v, v1); 542 | } 543 | 544 | @Override 545 | public Long zremrangeByScore(String s, String s1, String s2) { 546 | return cluster.zremrangeByScore(s, s1, s2); 547 | } 548 | 549 | @Override 550 | public Long zlexcount(String s, String s1, String s2) { 551 | return cluster.zlexcount(s, s1, s2); 552 | } 553 | 554 | @Override 555 | public Set zrangeByLex(String s, String s1, String s2) { 556 | return cluster.zrangeByLex(s, s1, s2); 557 | } 558 | 559 | @Override 560 | public Set zrangeByLex(String s, String s1, String s2, int i, int i1) { 561 | return cluster.zrangeByLex(s, s1, s2, i, i1); 562 | } 563 | 564 | @Override 565 | public Set zrevrangeByLex(String s, String s1, String s2) { 566 | return cluster.zrevrangeByLex(s, s1, s2); 567 | } 568 | 569 | @Override 570 | public Set zrevrangeByLex(String s, String s1, String s2, int i, int i1) { 571 | return cluster.zrevrangeByLex(s, s1, s2, i, i1); 572 | } 573 | 574 | @Override 575 | public Long zremrangeByLex(String s, String s1, String s2) { 576 | return cluster.zremrangeByLex(s, s1, s2); 577 | } 578 | 579 | @Override 580 | public Long linsert(String s, ListPosition listPosition, String s1, String s2) { 581 | return cluster.linsert(s, listPosition, s1, s2); 582 | } 583 | 584 | @Override 585 | public Long lpushx(String s, String... strings) { 586 | return cluster.lpushx(s, strings); 587 | } 588 | 589 | @Override 590 | public Long rpushx(String s, String... strings) { 591 | return cluster.rpushx(s, strings); 592 | } 593 | 594 | @Override 595 | public List blpop(int i, String s) { 596 | return cluster.blpop(i, s); 597 | } 598 | 599 | @Override 600 | public List brpop(int i, String s) { 601 | return cluster.brpop(i, s); 602 | } 603 | 604 | @Override 605 | public Long del(String s) { 606 | return cluster.del(s); 607 | } 608 | 609 | @Override 610 | public Long unlink(String s) { 611 | return cluster.unlink(s); 612 | } 613 | 614 | @Override 615 | public String echo(String s) { 616 | return cluster.echo(s); 617 | } 618 | 619 | @Override 620 | public Long move(String s, int i) { 621 | throw new UnsupportedOperationException(); 622 | } 623 | 624 | @Override 625 | public Long bitcount(String s) { 626 | return cluster.bitcount(s); 627 | } 628 | 629 | @Override 630 | public Long bitcount(String s, long l, long l1) { 631 | return cluster.bitcount(s, l, l1); 632 | } 633 | 634 | @Override 635 | public Long bitpos(String s, boolean b) { 636 | throw new UnsupportedOperationException(); 637 | } 638 | 639 | @Override 640 | public Long bitpos(String s, boolean b, BitPosParams bitPosParams) { 641 | throw new UnsupportedOperationException(); 642 | } 643 | 644 | @Override 645 | public ScanResult> hscan(String s, String s1) { 646 | return cluster.hscan(s, s1); 647 | } 648 | 649 | @Override 650 | public ScanResult> hscan(String s, String s1, ScanParams scanParams) { 651 | throw new UnsupportedOperationException(); 652 | } 653 | 654 | @Override 655 | public ScanResult sscan(String s, String s1) { 656 | return cluster.sscan(s, s1); 657 | } 658 | 659 | @Override 660 | public ScanResult zscan(String s, String s1) { 661 | return cluster.zscan(s, s1); 662 | } 663 | 664 | @Override 665 | public ScanResult zscan(String s, String s1, ScanParams scanParams) { 666 | return cluster.zscan(s.getBytes(), s1.getBytes(), scanParams); 667 | } 668 | 669 | @Override 670 | public ScanResult sscan(String s, String s1, ScanParams scanParams) { 671 | throw new UnsupportedOperationException(); 672 | } 673 | 674 | @Override 675 | public Long pfadd(String s, String... strings) { 676 | return cluster.pfadd(s, strings); 677 | } 678 | 679 | @Override 680 | public long pfcount(String s) { 681 | return cluster.pfcount(s); 682 | } 683 | 684 | @Override 685 | public Long geoadd(String s, double v, double v1, String s1) { 686 | return cluster.geoadd(s, v, v1, s1); 687 | } 688 | 689 | @Override 690 | public Long geoadd(String s, Map map) { 691 | return cluster.geoadd(s, map); 692 | } 693 | 694 | @Override 695 | public Double geodist(String s, String s1, String s2) { 696 | return cluster.geodist(s, s1, s2); 697 | } 698 | 699 | @Override 700 | public Double geodist(String s, String s1, String s2, GeoUnit geoUnit) { 701 | return cluster.geodist(s, s1, s2, geoUnit); 702 | } 703 | 704 | @Override 705 | public List geohash(String s, String... strings) { 706 | return cluster.geohash(s, strings); 707 | } 708 | 709 | @Override 710 | public List geopos(String s, String... strings) { 711 | return cluster.geopos(s, strings); 712 | } 713 | 714 | @Override 715 | public List georadius(String s, double v, double v1, double v2, GeoUnit geoUnit) { 716 | return cluster.georadius(s, v, v1, v2, geoUnit); 717 | } 718 | 719 | @Override 720 | public List georadiusReadonly(String s, double v, double v1, double v2, GeoUnit geoUnit) { 721 | return cluster.georadiusReadonly(s, v, v1, v2, geoUnit); 722 | } 723 | 724 | @Override 725 | public List georadius(String s, double v, double v1, double v2, GeoUnit geoUnit, GeoRadiusParam geoRadiusParam) { 726 | return cluster.georadiusReadonly(s, v, v1, v2, geoUnit, geoRadiusParam); 727 | } 728 | 729 | @Override 730 | public List georadiusReadonly(String s, double v, double v1, double v2, GeoUnit geoUnit, GeoRadiusParam geoRadiusParam) { 731 | return cluster.georadiusReadonly(s, v, v1, v2, geoUnit, geoRadiusParam); 732 | } 733 | 734 | @Override 735 | public List georadiusByMember(String s, String s1, double v, GeoUnit geoUnit) { 736 | return cluster.georadiusByMember(s, s1, v, geoUnit); 737 | } 738 | 739 | @Override 740 | public List georadiusByMemberReadonly(String s, String s1, double v, GeoUnit geoUnit) { 741 | return cluster.georadiusByMemberReadonly(s, s1, v, geoUnit); 742 | } 743 | 744 | @Override 745 | public List georadiusByMember(String s, String s1, double v, GeoUnit geoUnit, GeoRadiusParam geoRadiusParam) { 746 | return cluster.georadiusByMember(s, s1, v, geoUnit, geoRadiusParam); 747 | } 748 | 749 | @Override 750 | public List georadiusByMemberReadonly(String s, String s1, double v, GeoUnit geoUnit, GeoRadiusParam geoRadiusParam) { 751 | return cluster.georadiusByMemberReadonly(s, s1, v, geoUnit, geoRadiusParam); 752 | } 753 | 754 | @Override 755 | public List bitfield(String s, String... strings) { 756 | return cluster.bitfield(s, strings); 757 | } 758 | 759 | @Override 760 | public Long hstrlen(String s, String s1) { 761 | return cluster.hstrlen(s, s1); 762 | } 763 | 764 | 765 | @Override 766 | public Tuple zpopmax(String key) { 767 | return cluster.zpopmax(key); 768 | } 769 | 770 | @Override 771 | public Set zpopmax(String key, int count) { 772 | return cluster.zpopmax(key, count); 773 | } 774 | 775 | @Override 776 | public Tuple zpopmin(String key) { 777 | return cluster.zpopmin(key); 778 | } 779 | 780 | @Override 781 | public Set zpopmin(String key, int count) { 782 | return cluster.zpopmin(key, count); 783 | } 784 | 785 | @Override 786 | public List bitfieldReadonly(String key, String... arguments) { 787 | return cluster.bitfieldReadonly(key, arguments); 788 | } 789 | 790 | @Override 791 | public StreamEntryID xadd(String key, StreamEntryID id, Map hash) { 792 | return cluster.xadd(key, id, hash); 793 | } 794 | 795 | @Override 796 | public StreamEntryID xadd(String key, StreamEntryID id, Map hash, long maxLen, boolean approximateLength) { 797 | return cluster.xadd(key, id, hash, maxLen, approximateLength); 798 | } 799 | 800 | @Override 801 | public Long xlen(String key) { 802 | return cluster.xlen(key); 803 | } 804 | 805 | @Override 806 | public List xrange(String key, StreamEntryID start, StreamEntryID end, int count) { 807 | return cluster.xrange(key, start, end, count); 808 | } 809 | 810 | @Override 811 | public List xrevrange(String key, StreamEntryID end, StreamEntryID start, int count) { 812 | return cluster.xrevrange(key, end, start, count); 813 | } 814 | 815 | @Override 816 | public long xack(String key, String group, StreamEntryID... ids) { 817 | return cluster.xack(key, group, ids); 818 | } 819 | 820 | @Override 821 | public String xgroupCreate(String key, String groupname, StreamEntryID id, boolean makeStream) { 822 | return cluster.xgroupCreate(key, groupname, id, makeStream); 823 | } 824 | 825 | @Override 826 | public String xgroupSetID(String key, String groupname, StreamEntryID id) { 827 | return cluster.xgroupSetID(key, groupname, id); 828 | } 829 | 830 | @Override 831 | public long xgroupDestroy(String key, String groupname) { 832 | return cluster.xgroupDestroy(key, groupname); 833 | } 834 | 835 | @Override 836 | public Long xgroupDelConsumer(String key, String groupname, String consumername) { 837 | return cluster.xgroupDelConsumer(key, groupname, consumername); 838 | } 839 | 840 | @Override 841 | public List xpending(String key, String groupname, StreamEntryID start, StreamEntryID end, int count, String consumername) { 842 | return cluster.xpending(key, groupname, start, end, count, consumername); 843 | } 844 | 845 | @Override 846 | public long xdel(String key, StreamEntryID... ids) { 847 | return cluster.xdel(key, ids); 848 | } 849 | 850 | @Override 851 | public long xtrim(String key, long maxLen, boolean approximate) { 852 | return cluster.xtrim(key, maxLen, approximate); 853 | } 854 | 855 | @Override 856 | public List xclaim(String key, String group, String consumername, long minIdleTime, long newIdleTime, int retries, boolean force, StreamEntryID... ids) { 857 | return cluster.xclaim(key, group, consumername, minIdleTime, newIdleTime, retries, force, ids); 858 | } 859 | 860 | @Override 861 | public StreamInfo xinfoStream(String key) { 862 | throw new UnsupportedOperationException("xinfoStream not supported."); 863 | } 864 | 865 | @Override 866 | public List xinfoGroup(String key) { 867 | throw new UnsupportedOperationException("xinfoGroup not supported."); 868 | } 869 | 870 | @Override 871 | public List xinfoConsumers(String key, String group) { 872 | throw new UnsupportedOperationException("xinfoConsumers not supported"); 873 | } 874 | } 875 | -------------------------------------------------------------------------------- /src/main/java/net/joelinn/quartz/jobstore/mixin/CronTriggerMixin.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.jobstore.mixin; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import org.quartz.CronExpression; 6 | 7 | /** 8 | * Joe Linn 9 | * 7/15/2014 10 | */ 11 | public abstract class CronTriggerMixin extends TriggerMixin{ 12 | @JsonIgnore 13 | public abstract String getExpressionSummary(); 14 | 15 | @JsonIgnore 16 | public abstract void setCronExpression(CronExpression cron); 17 | 18 | @JsonProperty("cronExpression") 19 | public abstract void setCronExpression(String cronExpression); 20 | 21 | @JsonProperty("cronExpression") 22 | public abstract String getCronExpression(); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/joelinn/quartz/jobstore/mixin/HolidayCalendarMixin.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.jobstore.mixin; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import java.util.Date; 7 | import java.util.SortedSet; 8 | import java.util.TreeSet; 9 | 10 | /** 11 | * @author Joe Linn 12 | * 10/30/2016 13 | */ 14 | public class HolidayCalendarMixin { 15 | @JsonProperty 16 | private TreeSet dates; 17 | 18 | 19 | @JsonIgnore 20 | public SortedSet getExcludedDates() { 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/net/joelinn/quartz/jobstore/mixin/JobDetailMixin.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.jobstore.mixin; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import org.quartz.JobBuilder; 6 | import org.quartz.JobDataMap; 7 | import org.quartz.JobKey; 8 | 9 | /** 10 | * Joe Linn 11 | * 7/15/2014 12 | */ 13 | public abstract class JobDetailMixin { 14 | @JsonIgnore 15 | public abstract JobKey getKey(); 16 | 17 | @JsonIgnore 18 | public abstract JobDataMap getJobDataMap(); 19 | 20 | @JsonIgnore 21 | public abstract JobBuilder getJobBuilder(); 22 | 23 | @JsonIgnore 24 | public abstract String getFullName(); 25 | 26 | @JsonIgnore 27 | public abstract boolean isPersistJobDataAfterExecution(); 28 | 29 | @JsonIgnore 30 | public abstract boolean isConcurrentExectionDisallowed(); 31 | 32 | @JsonProperty("durable") 33 | public abstract void setDurability(boolean d); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/joelinn/quartz/jobstore/mixin/TriggerMixin.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.jobstore.mixin; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import org.quartz.*; 5 | 6 | import java.util.Date; 7 | 8 | /** 9 | * Joe Linn 10 | * 7/15/2014 11 | */ 12 | public abstract class TriggerMixin { 13 | @JsonIgnore 14 | public abstract TriggerBuilder getTriggerBuilder(); 15 | 16 | @JsonIgnore 17 | public abstract JobDataMap getJobDataMap(); 18 | 19 | @JsonIgnore 20 | public abstract JobKey getJobKey(); 21 | 22 | @JsonIgnore 23 | public abstract TriggerKey getKey(); 24 | 25 | @JsonIgnore 26 | public abstract String getFullName(); 27 | 28 | @JsonIgnore 29 | public abstract String getFullJobName(); 30 | 31 | @JsonIgnore 32 | public abstract Date getFinalFireTime(); 33 | 34 | @JsonIgnore 35 | public abstract ScheduleBuilder getScheduleBuilder(); 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/junit/Retry.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.junit; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author Joe Linn 10 | * 2/19/2018 11 | */ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target(ElementType.METHOD) 14 | public @interface Retry { 15 | int value() default 1; 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/junit/RetryRule.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.junit; 2 | 3 | import org.junit.rules.MethodRule; 4 | import org.junit.runners.model.FrameworkMethod; 5 | import org.junit.runners.model.Statement; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | /** 10 | * @author Joe Linn 11 | * 2/19/2018 12 | */ 13 | public class RetryRule implements MethodRule { 14 | private static final Logger log = LoggerFactory.getLogger(RetryRule.class); 15 | 16 | @Override 17 | public Statement apply(final Statement base, final FrameworkMethod method, Object target) { 18 | return new Statement() { 19 | @Override 20 | public void evaluate() throws Throwable { 21 | try { 22 | base.evaluate(); 23 | } catch (Throwable t) { 24 | Retry retry = method.getAnnotation(Retry.class); 25 | if (retry != null) { 26 | log.warn("Test " + method.getName() + " failed initial run. Retrying.", t); 27 | for (int i = 1; i <= retry.value(); i++) { 28 | try { 29 | base.evaluate(); 30 | } catch (Throwable innerThrowable) { 31 | if (i >= retry.value()) { 32 | throw innerThrowable; 33 | } else { 34 | log.warn("Test " + method.getName() + " failed on retry " + i, innerThrowable); 35 | } 36 | } 37 | } 38 | } else { 39 | throw t; 40 | } 41 | } 42 | } 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/BaseIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import com.google.common.base.Strings; 4 | import net.jodah.concurrentunit.Waiter; 5 | import net.joelinn.quartz.jobstore.RedisJobStore; 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.quartz.*; 9 | import org.quartz.impl.StdSchedulerFactory; 10 | import org.quartz.simpl.PropertySettingJobFactory; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import redis.clients.jedis.Jedis; 14 | import redis.clients.jedis.JedisPool; 15 | import redis.clients.jedis.util.Pool; 16 | import redis.embedded.RedisServer; 17 | 18 | import java.util.Properties; 19 | import java.util.concurrent.atomic.AtomicInteger; 20 | 21 | import static net.joelinn.quartz.TestUtils.getPort; 22 | 23 | /** 24 | * @author Joe Linn 25 | * 12/4/2016 26 | */ 27 | public abstract class BaseIntegrationTest { 28 | private static final Logger log = LoggerFactory.getLogger(BaseIntegrationTest.class); 29 | 30 | protected RedisServer redisServer; 31 | protected Scheduler scheduler; 32 | protected Pool jedisPool; 33 | 34 | protected int port; 35 | protected static final String HOST = "localhost"; 36 | 37 | 38 | @Before 39 | public void setUp() throws Exception { 40 | port = getPort(); 41 | redisServer = RedisServer.builder() 42 | .port(port) 43 | .build(); 44 | redisServer.start(); 45 | 46 | jedisPool = new JedisPool(HOST, port); 47 | 48 | 49 | scheduler = new StdSchedulerFactory(schedulerConfig(HOST, port)).getScheduler(); 50 | scheduler.start(); 51 | } 52 | 53 | 54 | protected Properties schedulerConfig(String host, int port) { 55 | Properties config = new Properties(); 56 | config.setProperty(StdSchedulerFactory.PROP_JOB_STORE_CLASS, RedisJobStore.class.getName()); 57 | config.setProperty("org.quartz.jobStore.host", host); 58 | config.setProperty("org.quartz.jobStore.port", String.valueOf(port)); 59 | config.setProperty("org.quartz.threadPool.threadCount", "1"); 60 | config.setProperty("org.quartz.jobStore.misfireThreshold", "500"); 61 | config.setProperty(StdSchedulerFactory.PROP_SCHED_SKIP_UPDATE_CHECK, "true"); 62 | return config; 63 | } 64 | 65 | 66 | @After 67 | public void tearDown() throws Exception { 68 | scheduler.shutdown(true); 69 | if (jedisPool != null) { 70 | jedisPool.close(); 71 | } 72 | redisServer.stop(); 73 | } 74 | 75 | 76 | public static class DataJob implements Job { 77 | protected Pool jedisPool; 78 | 79 | public void setJedisPool(Pool jedisPool) { 80 | this.jedisPool = jedisPool; 81 | } 82 | 83 | @Override 84 | public void execute(JobExecutionContext context) throws JobExecutionException { 85 | String foo = context.getTrigger().getJobDataMap().getString("foo"); 86 | if (!Strings.isNullOrEmpty(foo)) { 87 | try (Jedis jedis = jedisPool.getResource()) { 88 | jedis.set("foo", foo); 89 | } 90 | } else { 91 | log.error("Null or empty string retrieved from Redis."); 92 | } 93 | } 94 | } 95 | 96 | 97 | public static class SleepJob implements Job { 98 | @Override 99 | public void execute(JobExecutionContext context) throws JobExecutionException { 100 | try { 101 | Thread.sleep(1500); 102 | } catch (InterruptedException e) { 103 | throw new JobExecutionException("Interrupted while sleeping.", e); 104 | } 105 | } 106 | } 107 | 108 | 109 | @DisallowConcurrentExecution 110 | public static class SingletonSleepJob extends SleepJob { 111 | public static final AtomicInteger currentlyExecuting = new AtomicInteger(0); 112 | public static final AtomicInteger concurrentExecutions = new AtomicInteger(0); 113 | 114 | @Override 115 | public void execute(JobExecutionContext context) throws JobExecutionException { 116 | log.info("Starting job: " + context.getJobDetail().getKey() + " due to trigger " + context.getTrigger().getKey()); 117 | if (currentlyExecuting.incrementAndGet() > 1) { 118 | log.error("Concurrent execution detected!!"); 119 | concurrentExecutions.incrementAndGet(); 120 | throw new JobExecutionException("Concurrent execution not allowed!"); 121 | } 122 | try { 123 | Thread.sleep(1000); // add some extra sleep time to ensure that concurrent execution will be attempted 124 | } catch (InterruptedException e) { 125 | throw new JobExecutionException("Interrupted while sleeping.", e); 126 | } 127 | super.execute(context); 128 | currentlyExecuting.decrementAndGet(); 129 | } 130 | } 131 | 132 | 133 | protected class CompleteListener implements TriggerListener { 134 | private final Waiter waiter; 135 | 136 | protected CompleteListener(Waiter waiter) { 137 | this.waiter = waiter; 138 | } 139 | 140 | @Override 141 | public String getName() { 142 | return "Inigo Montoya"; 143 | } 144 | 145 | @Override 146 | public void triggerFired(Trigger trigger, JobExecutionContext context) { 147 | 148 | } 149 | 150 | @Override 151 | public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) { 152 | return false; 153 | } 154 | 155 | @Override 156 | public void triggerMisfired(Trigger trigger) { 157 | 158 | } 159 | 160 | @Override 161 | public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) { 162 | waiter.resume(); 163 | } 164 | } 165 | 166 | 167 | protected class MisfireListener implements TriggerListener { 168 | private final Waiter waiter; 169 | 170 | protected MisfireListener(Waiter waiter) { 171 | this.waiter = waiter; 172 | } 173 | 174 | @Override 175 | public String getName() { 176 | return "Rugen"; 177 | } 178 | 179 | @Override 180 | public void triggerFired(Trigger trigger, JobExecutionContext context) { 181 | 182 | } 183 | 184 | @Override 185 | public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) { 186 | return false; 187 | } 188 | 189 | @Override 190 | public void triggerMisfired(Trigger trigger) { 191 | waiter.resume(); 192 | } 193 | 194 | @Override 195 | public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) { 196 | 197 | } 198 | } 199 | 200 | 201 | protected class RedisJobFactory extends PropertySettingJobFactory { 202 | @Override 203 | protected void setBeanProps(Object obj, JobDataMap data) throws SchedulerException { 204 | data.put("jedisPool", jedisPool); 205 | super.setBeanProps(obj, data); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/BaseTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import net.joelinn.quartz.jobstore.RedisJobStore; 4 | import net.joelinn.quartz.jobstore.RedisJobStoreSchema; 5 | import org.junit.After; 6 | import org.junit.Before; 7 | import org.quartz.Calendar; 8 | import org.quartz.*; 9 | import org.quartz.impl.StdSchedulerFactory; 10 | import org.quartz.impl.calendar.WeeklyCalendar; 11 | import org.quartz.impl.triggers.CronTriggerImpl; 12 | import org.quartz.spi.SchedulerSignaler; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import redis.clients.jedis.Jedis; 16 | import redis.clients.jedis.JedisPool; 17 | import redis.clients.jedis.JedisPoolConfig; 18 | import redis.clients.jedis.Protocol; 19 | import redis.clients.jedis.util.Pool; 20 | import redis.embedded.RedisServer; 21 | 22 | import java.io.IOException; 23 | import java.util.*; 24 | 25 | import static net.joelinn.quartz.TestUtils.getPort; 26 | import static org.hamcrest.CoreMatchers.not; 27 | import static org.hamcrest.CoreMatchers.nullValue; 28 | import static org.hamcrest.MatcherAssert.assertThat; 29 | import static org.hamcrest.collection.IsMapContaining.hasKey; 30 | import static org.junit.Assert.assertEquals; 31 | import static org.mockito.Mockito.mock; 32 | 33 | /** 34 | * Joe Linn 35 | * 7/15/2014 36 | */ 37 | public abstract class BaseTest { 38 | private static final Logger logger = LoggerFactory.getLogger(BaseTest.class); 39 | 40 | protected RedisServer redisServer; 41 | 42 | protected Pool jedisPool; 43 | 44 | protected RedisJobStore jobStore; 45 | 46 | protected RedisJobStoreSchema schema; 47 | 48 | protected Jedis jedis; 49 | 50 | protected SchedulerSignaler mockScheduleSignaler; 51 | 52 | protected int port; 53 | 54 | protected String host = "localhost"; 55 | 56 | @Before 57 | public void setUpRedis() throws IOException, SchedulerConfigException { 58 | port = getPort(); 59 | logger.debug("Attempting to start embedded Redis server on port " + port); 60 | redisServer = RedisServer.builder() 61 | .port(port) 62 | .build(); 63 | redisServer.start(); 64 | final short database = 1; 65 | JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); 66 | jedisPoolConfig.setTestOnBorrow(true); 67 | jedisPool = new JedisPool(jedisPoolConfig, host, port, Protocol.DEFAULT_TIMEOUT, null, database); 68 | 69 | jobStore = new RedisJobStore(); 70 | jobStore.setHost(host); 71 | jobStore.setLockTimeout(2000); 72 | jobStore.setPort(port); 73 | jobStore.setInstanceId("testJobStore1"); 74 | jobStore.setDatabase(database); 75 | mockScheduleSignaler = mock(SchedulerSignaler.class); 76 | jobStore.initialize(null, mockScheduleSignaler); 77 | schema = new RedisJobStoreSchema(); 78 | 79 | jedis = jedisPool.getResource(); 80 | jedis.flushDB(); 81 | } 82 | 83 | 84 | @After 85 | public void tearDownRedis() throws InterruptedException { 86 | jedis.close(); 87 | jedisPool.destroy(); 88 | redisServer.stop(); 89 | } 90 | 91 | protected JobDetail getJobDetail(){ 92 | return getJobDetail("testJob", "testGroup"); 93 | } 94 | 95 | protected JobDetail getJobDetail(final String name, final String group){ 96 | return JobBuilder.newJob(TestJob.class) 97 | .withIdentity(name, group) 98 | .usingJobData("timeout", 42) 99 | .withDescription("I am describing a job!") 100 | .build(); 101 | } 102 | 103 | protected CronTriggerImpl getCronTrigger(){ 104 | String cron = "0/5 * * * * ?"; 105 | return (CronTriggerImpl) TriggerBuilder.newTrigger() 106 | .forJob("testJob", "testGroup") 107 | .withIdentity("testTrigger", "testTriggerGroup") 108 | .withSchedule(CronScheduleBuilder.cronSchedule(cron)) 109 | .usingJobData("timeout", 5) 110 | .withDescription("A description!") 111 | .build(); 112 | } 113 | 114 | protected CronTriggerImpl getCronTrigger(final String name, final String group, final JobKey jobKey){ 115 | return getCronTrigger(name, group, jobKey, "0 * * * * ?"); 116 | } 117 | 118 | protected CronTriggerImpl getCronTrigger(final String name, final String group, final JobKey jobKey, String cron){ 119 | CronTriggerImpl trigger = (CronTriggerImpl) TriggerBuilder.newTrigger() 120 | .forJob(jobKey) 121 | .withIdentity(name, group) 122 | .withSchedule(CronScheduleBuilder.cronSchedule(cron)) 123 | .usingJobData("timeout", 5) 124 | .withDescription("A description!") 125 | .build(); 126 | WeeklyCalendar calendar = new WeeklyCalendar(); 127 | calendar.setDaysExcluded(new boolean[]{false, false, false, false, false, false, false, false, false}); 128 | trigger.computeFirstFireTime(calendar); 129 | trigger.setCalendarName("testCalendar"); 130 | return trigger; 131 | } 132 | 133 | protected Calendar getCalendar(){ 134 | WeeklyCalendar calendar = new WeeklyCalendar(); 135 | // exclude weekends 136 | calendar.setDayExcluded(1, true); 137 | calendar.setDayExcluded(7, true); 138 | calendar.setDescription("Only run on weekdays."); 139 | return calendar; 140 | } 141 | 142 | protected Map> getJobsAndTriggers(int jobGroups, int jobsPerGroup, int triggerGroupsPerJob, int triggersPerGroup){ 143 | return getJobsAndTriggers(jobGroups, jobsPerGroup, triggerGroupsPerJob, triggersPerGroup, "0 * * * * ?"); 144 | } 145 | 146 | protected Map> getJobsAndTriggers(int jobGroups, int jobsPerGroup, int triggerGroupsPerJob, int triggersPerGroup, String cron){ 147 | Map> jobsAndTriggers = new HashMap<>(); 148 | for(int jobGroup = 0; jobGroup < jobGroups; jobGroup++){ 149 | String jobGroupName = String.format("jobGroup%s", jobGroup); 150 | for(int job = 0; job < jobsPerGroup; job++){ 151 | String jobName = String.format("%sjob%s", jobGroupName, job); 152 | JobDetail jobDetail = getJobDetail(jobName, jobGroupName); 153 | Set triggerSet = new HashSet<>(); 154 | for(int triggerGroup = 0; triggerGroup < triggerGroupsPerJob; triggerGroup++){ 155 | String triggerGroupName = String.format("%striggerGroup%s", jobName, triggerGroup); 156 | for(int trigger = 0; trigger < triggersPerGroup; trigger++){ 157 | String triggerName = String.format("%strigger%s", triggerGroupName, trigger); 158 | triggerSet.add(getCronTrigger(triggerName, triggerGroupName, jobDetail.getKey(), cron)); 159 | } 160 | } 161 | jobsAndTriggers.put(jobDetail, triggerSet); 162 | } 163 | } 164 | return jobsAndTriggers; 165 | } 166 | 167 | protected void storeJobAndTriggers(JobDetail job, Trigger... triggers) throws JobPersistenceException { 168 | Set triggersSet = new HashSet<>(triggers.length); 169 | Collections.addAll(triggersSet, triggers); 170 | Map> jobsAndTriggers = new HashMap<>(); 171 | jobsAndTriggers.put(job, triggersSet); 172 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 173 | } 174 | 175 | protected void testJobStore(Properties quartzProperties) throws SchedulerException { 176 | StdSchedulerFactory schedulerFactory = new StdSchedulerFactory(); 177 | schedulerFactory.initialize(quartzProperties); 178 | 179 | Scheduler scheduler = schedulerFactory.getScheduler(); 180 | scheduler.start(); 181 | 182 | JobDetail job = getJobDetail("testJob1", "testJobGroup1"); 183 | CronTriggerImpl trigger = getCronTrigger("testTrigger1", "testTriggerGroup1", job.getKey(), "0/5 * * * * ?"); 184 | 185 | scheduler.scheduleJob(job, trigger); 186 | 187 | JobDetail retrievedJob = jobStore.retrieveJob(job.getKey()); 188 | assertThat(retrievedJob, not(nullValue())); 189 | assertThat(retrievedJob.getJobDataMap(), hasKey("timeout")); 190 | 191 | CronTriggerImpl retrievedTrigger = (CronTriggerImpl) jobStore.retrieveTrigger(trigger.getKey()); 192 | assertThat(retrievedTrigger, not(nullValue())); 193 | assertEquals(trigger.getCronExpression(), retrievedTrigger.getCronExpression()); 194 | 195 | scheduler.deleteJob(job.getKey()); 196 | 197 | assertThat(jobStore.retrieveJob(job.getKey()), nullValue()); 198 | assertThat(jobStore.retrieveTrigger(trigger.getKey()), nullValue()); 199 | 200 | scheduler.shutdown(); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/MultiSchedulerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import net.jodah.concurrentunit.Waiter; 4 | import net.joelinn.junit.Retry; 5 | import net.joelinn.junit.RetryRule; 6 | import net.joelinn.quartz.jobstore.RedisJobStoreSchema; 7 | import org.hamcrest.Matchers; 8 | import org.junit.After; 9 | import org.junit.Before; 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.quartz.*; 13 | import org.quartz.impl.StdSchedulerFactory; 14 | import org.quartz.impl.matchers.NameMatcher; 15 | import org.quartz.simpl.SimpleInstanceIdGenerator; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import redis.clients.jedis.Jedis; 19 | 20 | import java.util.List; 21 | import java.util.Properties; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | import static junit.framework.TestCase.fail; 25 | import static net.joelinn.quartz.TestUtils.createCronTrigger; 26 | import static net.joelinn.quartz.TestUtils.createJob; 27 | import static org.hamcrest.MatcherAssert.assertThat; 28 | import static org.hamcrest.Matchers.*; 29 | 30 | /** 31 | * @author Joe Linn 32 | * 12/10/2016 33 | */ 34 | public class MultiSchedulerIntegrationTest extends BaseIntegrationTest { 35 | private static final Logger log = LoggerFactory.getLogger(MultiSchedulerIntegrationTest.class); 36 | 37 | private static final String KEY_ID = "id"; 38 | 39 | 40 | @Rule 41 | public RetryRule retryRule = new RetryRule(); 42 | 43 | private Scheduler scheduler2; 44 | private RedisJobStoreSchema schema; 45 | 46 | private static String jobThreadName; 47 | private static Waiter jobStartWaiter; 48 | 49 | @Before 50 | @Override 51 | public void setUp() throws Exception { 52 | super.setUp(); 53 | Properties props = schedulerConfig(HOST, port); 54 | props.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, "second"); 55 | props.setProperty(StdSchedulerFactory.PROP_SCHED_BATCH_TIME_WINDOW, "500"); 56 | props.setProperty(StdSchedulerFactory.PROP_SCHED_IDLE_WAIT_TIME, "1000"); 57 | scheduler2 = new StdSchedulerFactory(props).getScheduler(); 58 | schema = new RedisJobStoreSchema(); 59 | } 60 | 61 | 62 | @After 63 | @Override 64 | public void tearDown() throws Exception { 65 | scheduler2.shutdown(true); 66 | super.tearDown(); 67 | } 68 | 69 | 70 | @Override 71 | protected Properties schedulerConfig(String host, int port) { 72 | Properties config = super.schedulerConfig(host, port); 73 | config.setProperty("org.quartz.threadPool.threadCount", "2"); 74 | config.setProperty("org.quartz.scheduler.instanceId", "AUTO"); 75 | config.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID_GENERATOR_CLASS, SimpleInstanceIdGenerator.class.getName()); 76 | return config; 77 | } 78 | 79 | @Test 80 | @Retry(5) 81 | public void testMultipleSchedulers() throws Exception { 82 | scheduler.setJobFactory(new RedisJobFactory()); 83 | scheduler2.setJobFactory(new RedisJobFactory()); 84 | 85 | assertThat(scheduler.getSchedulerInstanceId(), notNullValue()); 86 | assertThat(scheduler2.getSchedulerInstanceId(), notNullValue()); 87 | assertThat(scheduler.getSchedulerInstanceId(), not(equalTo(scheduler2.getSchedulerInstanceId()))); 88 | 89 | JobDetail job = createJob(SchedulerIDCheckingJob.class, "testJob", "group"); 90 | final String triggerName = "test-trigger"; 91 | CronTrigger trigger = createCronTrigger(triggerName, "group", "* * * * * ?"); 92 | 93 | Waiter waiter = new Waiter(); 94 | scheduler.getListenerManager().addTriggerListener(new CompleteListener(waiter), NameMatcher.triggerNameEquals(triggerName)); 95 | scheduler.scheduleJob(job, trigger); 96 | 97 | waiter.await(1500); 98 | 99 | try (Jedis jedis = jedisPool.getResource()) { 100 | assertThat(jedis.get(KEY_ID), equalTo(scheduler.getSchedulerInstanceId())); 101 | } 102 | 103 | scheduler.shutdown(true); 104 | waiter = new Waiter(); 105 | scheduler2.getListenerManager().addTriggerListener(new CompleteListener(waiter), NameMatcher.triggerNameEquals(triggerName)); 106 | if (log.isDebugEnabled()) { 107 | log.debug("Starting second scheduler."); 108 | } 109 | scheduler2.start(); 110 | 111 | waiter.await(1500); 112 | 113 | try (Jedis jedis = jedisPool.getResource()) { 114 | assertThat(jedis.get(KEY_ID), equalTo(scheduler2.getSchedulerInstanceId())); 115 | } 116 | 117 | List triggers = scheduler2.getTriggersOfJob(job.getKey()); 118 | assertThat(triggers, Matchers.hasSize(greaterThan(0))); 119 | for (Trigger t : triggers) { 120 | assertThat(t.getPreviousFireTime(), notNullValue()); 121 | } 122 | } 123 | 124 | 125 | @Test 126 | public void testDeadScheduler() throws Exception { 127 | scheduler.setJobFactory(new RedisJobFactory()); 128 | scheduler2.setJobFactory(new RedisJobFactory()); 129 | 130 | assertThat(scheduler.getSchedulerInstanceId(), notNullValue()); 131 | assertThat(scheduler2.getSchedulerInstanceId(), notNullValue()); 132 | assertThat(scheduler.getSchedulerInstanceId(), not(equalTo(scheduler2.getSchedulerInstanceId()))); 133 | 134 | JobDetail job = createJob(SchedulerIDCheckingJob.class, "testJob", "group") 135 | .getJobBuilder() 136 | .usingJobData("sleep", 15_000L) 137 | .build(); 138 | final String triggerName = "test-trigger"; 139 | CronTrigger trigger = createCronTrigger(triggerName, "group", "* * * * * ?"); 140 | 141 | jobStartWaiter = new Waiter(); 142 | scheduler.scheduleJob(job, trigger); 143 | jobStartWaiter.await(1500, TimeUnit.MILLISECONDS); 144 | 145 | scheduler.shutdown(false); 146 | getThreadByName(jobThreadName).interrupt(); 147 | 148 | try (Jedis jedis = jedisPool.getResource()) { 149 | assertThat(jedis.get(KEY_ID), equalTo(scheduler.getSchedulerInstanceId())); 150 | assertThat(jedis.exists(schema.lastInstanceActiveTime()), equalTo(true)); 151 | assertThat(jedis.hexists(schema.lastInstanceActiveTime(), scheduler.getSchedulerInstanceId()), equalTo(true)); 152 | jedis.hset(schema.lastInstanceActiveTime(), scheduler.getSchedulerInstanceId(), String.valueOf(System.currentTimeMillis() - 5 * 60_000)); 153 | assertThat("job still blocked", jedis.sismember(schema.blockedJobsSet(), schema.jobHashKey(job.getKey())), equalTo(true)); 154 | } 155 | 156 | scheduler2.start(); 157 | jobStartWaiter.await(1500, TimeUnit.MILLISECONDS); 158 | 159 | getThreadByName(jobThreadName).interrupt(); 160 | 161 | try (Jedis jedis = jedisPool.getResource()) { 162 | assertThat(jedis.get(KEY_ID), equalTo(scheduler2.getSchedulerInstanceId())); 163 | } 164 | } 165 | 166 | 167 | private Thread getThreadByName(String threadName) { 168 | for (Thread t : Thread.getAllStackTraces().keySet()) { 169 | if (t.getName().equals(threadName)) { 170 | return t; 171 | } 172 | } 173 | return null; 174 | } 175 | 176 | 177 | @DisallowConcurrentExecution 178 | public static class SchedulerIDCheckingJob extends DataJob { 179 | @Override 180 | public void execute(JobExecutionContext context) throws JobExecutionException { 181 | jobThreadName = Thread.currentThread().getName(); 182 | try { 183 | final String schedulerID = context.getScheduler().getSchedulerInstanceId(); 184 | try (Jedis jedis = jedisPool.getResource()) { 185 | if (jedis.setnx(KEY_ID, schedulerID) == 0) { 186 | // we already have an ID stored 187 | final String storedID = jedis.get(KEY_ID); 188 | if (storedID.equals(schedulerID)) { 189 | fail("The same schedule executed the job twice."); 190 | } else { 191 | jedis.set(KEY_ID, schedulerID); 192 | } 193 | } 194 | } 195 | if (log.isDebugEnabled()) { 196 | log.debug("Completed job on behalf of scheduler {} at {}", schedulerID, System.currentTimeMillis()); 197 | } 198 | if (jobStartWaiter != null) { 199 | jobStartWaiter.resume(); 200 | } 201 | JobDataMap dataMap = context.getMergedJobDataMap(); 202 | if (dataMap.containsKey("sleep")) { 203 | try { 204 | Thread.sleep(dataMap.getLong("sleep")); 205 | } catch (InterruptedException e) { 206 | if (log.isDebugEnabled()) { 207 | log.debug("Job interrupted while sleeping.", e); 208 | } 209 | } 210 | } 211 | } catch (SchedulerException e) { 212 | log.error("Unable to obtain scheduler instance ID.", e); 213 | fail("Failed to obtain scheduler instance ID."); 214 | } 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/MultiThreadedIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import net.jodah.concurrentunit.Waiter; 4 | import org.junit.Test; 5 | import org.quartz.CronTrigger; 6 | import org.quartz.JobDetail; 7 | import org.quartz.impl.matchers.NameMatcher; 8 | import redis.clients.jedis.Jedis; 9 | 10 | import java.util.Properties; 11 | 12 | import static net.joelinn.quartz.TestUtils.createCronTrigger; 13 | import static net.joelinn.quartz.TestUtils.createJob; 14 | import static org.hamcrest.CoreMatchers.equalTo; 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | 17 | /** 18 | * @author Joe Linn 19 | * 10/4/2016 20 | */ 21 | public class MultiThreadedIntegrationTest extends BaseIntegrationTest { 22 | 23 | @Override 24 | protected Properties schedulerConfig(String host, int port) { 25 | Properties config = super.schedulerConfig(host, port); 26 | config.setProperty("org.quartz.threadPool.threadCount", "2"); 27 | return config; 28 | } 29 | 30 | @Test 31 | public void testCompleteListener() throws Exception { 32 | final String jobName = "oneJob"; 33 | JobDetail jobDetail = createJob(TestJob.class, jobName, "oneGroup"); 34 | 35 | final String triggerName = "trigger1"; 36 | CronTrigger trigger = createCronTrigger(triggerName, "oneGroup", "* * * * * ?"); 37 | 38 | Waiter waiter = new Waiter(); 39 | scheduler.scheduleJob(jobDetail, trigger); 40 | scheduler.getListenerManager().addTriggerListener(new CompleteListener(waiter), NameMatcher.triggerNameEquals(triggerName)); 41 | 42 | // wait for CompleteListener.triggerComplete() to be called 43 | waiter.await(1500); 44 | } 45 | 46 | 47 | @Test 48 | public void testTriggerData() throws Exception { 49 | final String jobName = "good"; 50 | JobDetail jobDetail = createJob(DataJob.class, jobName, "goodGroup"); 51 | 52 | final String triggerName = "trigger1"; 53 | final String everySecond = "* * * * * ?"; 54 | CronTrigger trigger = createCronTrigger(triggerName, "oneGroup", everySecond); 55 | trigger = trigger.getTriggerBuilder() 56 | .usingJobData("foo", "bar") 57 | .build(); 58 | scheduler.setJobFactory(new RedisJobFactory()); 59 | scheduler.scheduleJob(jobDetail, trigger); 60 | Waiter waiter = new Waiter(); 61 | scheduler.getListenerManager().addTriggerListener(new CompleteListener(waiter), NameMatcher.triggerNameEquals(triggerName)); 62 | 63 | // wait for CompleteListener.triggerComplete() to be called 64 | waiter.await(1500); 65 | 66 | try (Jedis jedis = jedisPool.getResource()) { 67 | assertThat(jedis.get("foo"), equalTo("bar")); 68 | } 69 | } 70 | 71 | 72 | @Test 73 | public void testDisallowConcurrent() throws Exception { 74 | JobDetail job1 = createJob(SingletonSleepJob.class, "job1", "group1"); 75 | CronTrigger trigger1 = createCronTrigger("trigger1", "group1", "* * * * * ?"); 76 | CronTrigger trigger2 = createCronTrigger("trigger2", "group2", "* * * * * ?") 77 | .getTriggerBuilder() 78 | .forJob(job1) 79 | .build(); 80 | 81 | Waiter waiter = new Waiter(); 82 | scheduler.getListenerManager().addTriggerListener(new CompleteListener(waiter), NameMatcher.triggerNameEquals(trigger1.getKey().getName())); 83 | scheduler.scheduleJob(job1, trigger1); 84 | //scheduler.scheduleJob(trigger2); 85 | 86 | waiter.await(6000, 2); 87 | 88 | assertThat(SingletonSleepJob.concurrentExecutions.get(), equalTo(0)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/RedisJobStoreTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import net.joelinn.quartz.jobstore.RedisJobStore; 4 | import org.junit.Test; 5 | import org.quartz.JobDetail; 6 | import org.quartz.Trigger; 7 | 8 | import java.util.Map; 9 | import java.util.Properties; 10 | import java.util.Set; 11 | 12 | import static org.junit.Assert.assertEquals; 13 | 14 | /** 15 | * Joe Linn 16 | * 7/22/2014 17 | */ 18 | public class RedisJobStoreTest extends BaseTest{ 19 | 20 | @Test 21 | public void redisJobStoreWithScheduler() throws Exception { 22 | Properties quartzProperties = new Properties(); 23 | quartzProperties.setProperty("org.quartz.scheduler.instanceName", "testScheduler"); 24 | quartzProperties.setProperty("org.quartz.threadPool.threadCount", "3"); 25 | quartzProperties.setProperty("org.quartz.jobStore.class", RedisJobStore.class.getName()); 26 | quartzProperties.setProperty("org.quartz.jobStore.host", host); 27 | quartzProperties.setProperty("org.quartz.jobStore.port", Integer.toString(port)); 28 | quartzProperties.setProperty("org.quartz.jobStore.lockTimeout", "2000"); 29 | quartzProperties.setProperty("org.quartz.jobStore.database", "1"); 30 | 31 | testJobStore(quartzProperties); 32 | } 33 | 34 | @Test 35 | public void clearAllSchedulingData() throws Exception { 36 | // create and store some jobs, triggers, and calendars 37 | Map> jobsAndTriggers = getJobsAndTriggers(2, 2, 2, 2); 38 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 39 | 40 | // ensure that the jobs, triggers, and calendars were stored 41 | assertEquals(2, (long) jedis.scard(schema.jobGroupsSet())); 42 | assertEquals(4, (long) jedis.scard(schema.jobsSet())); 43 | assertEquals(8, (long) jedis.scard(schema.triggerGroupsSet())); 44 | assertEquals(16, (long) jedis.scard(schema.triggersSet())); 45 | 46 | jobStore.clearAllSchedulingData(); 47 | 48 | assertEquals(0, (long) jedis.scard(schema.jobGroupsSet())); 49 | assertEquals(0, (long) jedis.scard(schema.jobsSet())); 50 | assertEquals(0, (long) jedis.scard(schema.triggerGroupsSet())); 51 | assertEquals(0, (long) jedis.scard(schema.triggersSet())); 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/RedisSentinelJobStoreTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | 4 | import com.google.common.base.Joiner; 5 | import net.joelinn.quartz.jobstore.RedisJobStore; 6 | import net.joelinn.quartz.jobstore.RedisJobStoreSchema; 7 | import org.junit.After; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.quartz.SchedulerConfigException; 11 | import org.quartz.spi.SchedulerSignaler; 12 | import redis.clients.jedis.JedisPoolConfig; 13 | import redis.clients.jedis.JedisSentinelPool; 14 | import redis.embedded.RedisCluster; 15 | import redis.embedded.util.JedisUtil; 16 | 17 | import java.io.IOException; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.Properties; 21 | import java.util.Set; 22 | 23 | import static net.joelinn.quartz.TestUtils.getPort; 24 | import static org.mockito.Mockito.mock; 25 | 26 | public class RedisSentinelJobStoreTest extends BaseTest { 27 | 28 | private JedisSentinelPool jedisSentinelPool; 29 | private String joinedHosts; 30 | private RedisCluster redisCluster; 31 | 32 | @Before 33 | public void setUpRedis() throws IOException, SchedulerConfigException { 34 | final List sentinels = Arrays.asList(getPort(), getPort()); 35 | final List group1 = Arrays.asList(getPort(), getPort()); 36 | final List group2 = Arrays.asList(getPort(), getPort()); 37 | //creates a cluster with 3 sentinels, quorum size of 2 and 3 replication groups, each with one master and one slave 38 | redisCluster = RedisCluster.builder().sentinelPorts(sentinels).quorumSize(2) 39 | .serverPorts(group1).replicationGroup("master1", 1) 40 | .serverPorts(group2).replicationGroup("master2", 1) 41 | .ephemeralServers().replicationGroup("master3", 1) 42 | .build(); 43 | redisCluster.start(); 44 | 45 | 46 | Set jedisSentinelHosts = JedisUtil.sentinelHosts(redisCluster); 47 | 48 | joinedHosts = Joiner.on(",").join(jedisSentinelHosts); 49 | 50 | final short database = 1; 51 | JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); 52 | jedisPoolConfig.setTestOnBorrow(true); 53 | jedisPoolConfig.setTestOnCreate(true); 54 | jedisPoolConfig.setTestOnReturn(true); 55 | jedisPoolConfig.setMaxWaitMillis(2000); 56 | jedisPoolConfig.setMaxTotal(20); 57 | jedisPool = new JedisSentinelPool("master1", jedisSentinelHosts, jedisPoolConfig); 58 | jobStore = new RedisJobStore(); 59 | jobStore.setHost(joinedHosts); 60 | jobStore.setJedisPool(jedisSentinelPool); 61 | jobStore.setLockTimeout(2000); 62 | jobStore.setMasterGroupName("master1"); 63 | jobStore.setRedisSentinel(true); 64 | jobStore.setInstanceId("testJobStore1"); 65 | jobStore.setDatabase(database); 66 | mockScheduleSignaler = mock(SchedulerSignaler.class); 67 | jobStore.initialize(null, mockScheduleSignaler); 68 | schema = new RedisJobStoreSchema(); 69 | 70 | jedis = jedisPool.getResource(); 71 | jedis.flushDB(); 72 | 73 | } 74 | 75 | @After 76 | public void tearDownRedis() throws InterruptedException { 77 | if (jedis != null) { 78 | jedis.close(); 79 | } 80 | if (jedisPool != null) { 81 | jedisPool.close(); 82 | } 83 | redisCluster.stop(); 84 | } 85 | 86 | @Test 87 | public void redisSentinelJobStoreWithScheduler() throws Exception { 88 | Properties quartzProperties = new Properties(); 89 | quartzProperties.setProperty("org.quartz.scheduler.instanceName", "testScheduler"); 90 | quartzProperties.setProperty("org.quartz.threadPool.threadCount", "3"); 91 | quartzProperties.setProperty("org.quartz.jobStore.class", RedisJobStore.class.getName()); 92 | quartzProperties.setProperty("org.quartz.jobStore.host", joinedHosts); 93 | quartzProperties.setProperty("org.quartz.jobStore.redisSentinel", String.valueOf(true)); 94 | quartzProperties.setProperty("org.quartz.jobStore.masterGroupName", "master1"); 95 | quartzProperties.setProperty("org.quartz.jobStore.lockTimeout", "2000"); 96 | quartzProperties.setProperty("org.quartz.jobStore.database", "1"); 97 | testJobStore(quartzProperties); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/SingleThreadedIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import net.jodah.concurrentunit.Waiter; 4 | import org.junit.Test; 5 | import org.quartz.CronTrigger; 6 | import org.quartz.JobDetail; 7 | import org.quartz.SimpleTrigger; 8 | import org.quartz.TriggerBuilder; 9 | import org.quartz.impl.matchers.NameMatcher; 10 | 11 | import static net.joelinn.quartz.TestUtils.createCronTrigger; 12 | import static net.joelinn.quartz.TestUtils.createJob; 13 | import static org.quartz.SimpleScheduleBuilder.simpleSchedule; 14 | 15 | /** 16 | * @author Joe Linn 17 | * 12/4/2016 18 | */ 19 | public class SingleThreadedIntegrationTest extends BaseIntegrationTest { 20 | @Test 21 | public void testMisfireListener() throws Exception { 22 | final String jobName = "oneJob"; 23 | JobDetail jobDetail = createJob(TestJob.class, jobName, "oneGroup"); 24 | 25 | final String triggerName = "trigger1"; 26 | final String everySecond = "* * * * * ?"; 27 | CronTrigger trigger = createCronTrigger(triggerName, "oneGroup", everySecond); 28 | 29 | 30 | JobDetail sleepJob = createJob(SleepJob.class, "sleepJob", "twoGroup"); 31 | CronTrigger sleepTrigger = createCronTrigger("sleepTrigger", "twoGroup", everySecond); 32 | Waiter waiter = new Waiter(); 33 | scheduler.scheduleJob(sleepJob, sleepTrigger); 34 | scheduler.scheduleJob(jobDetail, trigger); 35 | 36 | scheduler.getListenerManager().addTriggerListener(new MisfireListener(waiter), NameMatcher.triggerNameEquals(triggerName)); 37 | 38 | // wait for MisfireListener.triggerMisfired() to be called 39 | waiter.await(2500); 40 | } 41 | 42 | 43 | @Test 44 | public void testSingleExecution() throws Exception { 45 | final String jobName = "oneJob"; 46 | JobDetail jobDetail = createJob(TestJob.class, jobName, "oneGroup"); 47 | 48 | SimpleTrigger trigger = TriggerBuilder.newTrigger().withSchedule(simpleSchedule().withRepeatCount(0).withIntervalInMilliseconds(200)).build(); 49 | 50 | Waiter waiter = new Waiter(); 51 | scheduler.getListenerManager().addTriggerListener(new CompleteListener(waiter), NameMatcher.triggerNameEquals(trigger.getKey().getName())); 52 | 53 | scheduler.scheduleJob(jobDetail, trigger); 54 | 55 | waiter.await(2000); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/StoreCalendarTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.junit.Test; 6 | import org.quartz.Calendar; 7 | import org.quartz.JobDetail; 8 | import org.quartz.JobPersistenceException; 9 | import org.quartz.impl.calendar.HolidayCalendar; 10 | import org.quartz.impl.triggers.CronTriggerImpl; 11 | 12 | import java.util.Date; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static org.hamcrest.CoreMatchers.*; 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.containsInAnyOrder; 20 | import static org.hamcrest.Matchers.hasSize; 21 | import static org.hamcrest.collection.IsMapContaining.hasKey; 22 | import static org.junit.Assert.*; 23 | 24 | /** 25 | * Joe Linn 26 | * 7/17/2014 27 | */ 28 | public class StoreCalendarTest extends BaseTest{ 29 | @Test 30 | public void storeCalendar() throws Exception { 31 | final String calendarName = "weekdayCalendar"; 32 | Calendar calendar = getCalendar(); 33 | 34 | jobStore.storeCalendar(calendarName, calendar, false, false); 35 | 36 | final String calendarHashKey = schema.calendarHashKey(calendarName); 37 | Map calendarMap = jedis.hgetAll(calendarHashKey); 38 | 39 | assertThat(calendarMap, hasKey("calendar_class")); 40 | assertEquals(calendar.getClass().getName(), calendarMap.get("calendar_class")); 41 | assertThat(calendarMap, hasKey("calendar_json")); 42 | 43 | ObjectMapper mapper = new ObjectMapper(); 44 | 45 | Map calendarJson = mapper.readValue(calendarMap.get("calendar_json"), new TypeReference>() { 46 | }); 47 | assertThat(calendarJson, hasKey("description")); 48 | assertEquals("Only run on weekdays.", calendarJson.get("description")); 49 | } 50 | 51 | @Test 52 | public void storeCalendarWithReplace() throws Exception { 53 | final String calendarName = "weekdayCalendar"; 54 | Calendar calendar = getCalendar(); 55 | jobStore.storeCalendar(calendarName, calendar, true, false); 56 | jobStore.storeCalendar(calendarName, calendar, true, false); 57 | } 58 | 59 | @Test(expected = JobPersistenceException.class) 60 | public void storeCalendarNoReplace() throws Exception { 61 | final String calendarName = "weekdayCalendar"; 62 | Calendar calendar = getCalendar(); 63 | jobStore.storeCalendar(calendarName, calendar, false, false); 64 | jobStore.storeCalendar(calendarName, calendar, false, false); 65 | } 66 | 67 | @Test 68 | public void retrieveCalendar() throws Exception { 69 | final String calendarName = "weekdayCalendar"; 70 | Calendar calendar = getCalendar(); 71 | jobStore.storeCalendar(calendarName, calendar, false, false); 72 | 73 | Calendar retrievedCalendar = jobStore.retrieveCalendar(calendarName); 74 | 75 | assertEquals(calendar.getClass(), retrievedCalendar.getClass()); 76 | assertEquals(calendar.getDescription(), retrievedCalendar.getDescription()); 77 | long currentTime = System.currentTimeMillis(); 78 | assertEquals(calendar.getNextIncludedTime(currentTime), retrievedCalendar.getNextIncludedTime(currentTime)); 79 | } 80 | 81 | @Test 82 | public void getNumberOfCalendars() throws Exception { 83 | jobStore.storeCalendar("calendar1", getCalendar(), false, false); 84 | jobStore.storeCalendar("calendar1", getCalendar(), true, false); 85 | jobStore.storeCalendar("calendar2", getCalendar(), false, false); 86 | 87 | int numberOfCalendars = jobStore.getNumberOfCalendars(); 88 | 89 | assertEquals(2, numberOfCalendars); 90 | } 91 | 92 | @Test 93 | public void getCalendarNames() throws Exception { 94 | List calendarNames = jobStore.getCalendarNames(); 95 | 96 | assertThat(calendarNames, not(nullValue())); 97 | assertThat(calendarNames, hasSize(0)); 98 | 99 | jobStore.storeCalendar("calendar1", getCalendar(), false, false); 100 | jobStore.storeCalendar("calendar2", getCalendar(), false, false); 101 | 102 | calendarNames = jobStore.getCalendarNames(); 103 | 104 | assertThat(calendarNames, hasSize(2)); 105 | assertThat(calendarNames, containsInAnyOrder("calendar2", "calendar1")); 106 | } 107 | 108 | @Test 109 | public void removeCalendar() throws Exception { 110 | assertFalse(jobStore.removeCalendar("foo")); 111 | 112 | jobStore.storeCalendar("calendar1", getCalendar(), false, false); 113 | 114 | assertTrue(jobStore.removeCalendar("calendar1")); 115 | 116 | assertThat(jobStore.retrieveCalendar("calendar1"), nullValue()); 117 | } 118 | 119 | @Test(expected = JobPersistenceException.class) 120 | public void removeCalendarWithTrigger() throws Exception { 121 | // store trigger and job 122 | JobDetail job = getJobDetail(); 123 | jobStore.storeJob(job, false); 124 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey()); 125 | jobStore.storeTrigger(trigger1, false); 126 | 127 | jobStore.removeCalendar(trigger1.getCalendarName()); 128 | } 129 | 130 | @Test 131 | public void holidayCalendar() throws Exception { 132 | // HolidayCalendar sets the time of any given Date to 00:00:00 133 | java.util.Calendar cal = java.util.Calendar.getInstance(); 134 | cal.set(java.util.Calendar.HOUR_OF_DAY, 0); 135 | cal.set(java.util.Calendar.MINUTE, 0); 136 | cal.set(java.util.Calendar.SECOND, 0); 137 | cal.set(java.util.Calendar.MILLISECOND, 0); 138 | final Date excludedDate = cal.getTime(); 139 | 140 | HolidayCalendar calendar = new HolidayCalendar(); 141 | calendar.addExcludedDate(excludedDate); 142 | final String name = "holidayCalendar"; 143 | jobStore.storeCalendar(name, calendar, true, true); 144 | 145 | final String calendarHashKey = schema.calendarHashKey(name); 146 | Map calendarMap = jedis.hgetAll(calendarHashKey); 147 | 148 | assertThat(calendarMap, hasKey("calendar_class")); 149 | assertThat(calendarMap.get("calendar_class"), equalTo(HolidayCalendar.class.getName())); 150 | assertThat(calendarMap, hasKey("calendar_json")); 151 | String json = calendarMap.get("calendar_json"); 152 | assertThat(json, containsString("\"dates\":[")); 153 | assertThat(json, not(containsString("\"excludedDates\":"))); 154 | 155 | Calendar retrieved = jobStore.retrieveCalendar(name); 156 | assertThat(retrieved, notNullValue()); 157 | assertThat(retrieved, instanceOf(HolidayCalendar.class)); 158 | HolidayCalendar retrievedHoliday = (HolidayCalendar) retrieved; 159 | assertThat(retrievedHoliday.getExcludedDates(), hasItem(excludedDate)); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/StoreJobTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import org.junit.Test; 4 | import org.quartz.*; 5 | import org.quartz.impl.matchers.GroupMatcher; 6 | import org.quartz.impl.triggers.CronTriggerImpl; 7 | 8 | import java.util.*; 9 | 10 | import static org.hamcrest.CoreMatchers.not; 11 | import static org.hamcrest.CoreMatchers.nullValue; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.containsInAnyOrder; 14 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 15 | import static org.hamcrest.collection.IsMapContaining.hasKey; 16 | import static org.junit.Assert.*; 17 | 18 | /** 19 | * Joe Linn 20 | * 7/15/2014 21 | */ 22 | public class StoreJobTest extends BaseTest{ 23 | 24 | @Test 25 | public void storeJob() throws Exception { 26 | JobDetail testJob = getJobDetail(); 27 | 28 | jobStore.storeJob(testJob, false); 29 | 30 | // ensure that the job was stored properly 31 | String jobHashKey = schema.jobHashKey(testJob.getKey()); 32 | Map jobMap = jedis.hgetAll(jobHashKey); 33 | 34 | assertNotNull(jobMap); 35 | assertThat(jobMap, hasKey("name")); 36 | assertEquals("testJob", jobMap.get("name")); 37 | 38 | Map jobData = jedis.hgetAll(schema.jobDataMapHashKey(testJob.getKey())); 39 | assertNotNull(jobData); 40 | assertThat(jobData, hasKey("timeout")); 41 | assertEquals("42", jobData.get("timeout")); 42 | 43 | // ensure that job data which is not included in the current map is removed from Redis 44 | testJob.getJobDataMap().remove("timeout"); 45 | testJob.getJobDataMap().put("foo", "bar"); 46 | jobStore.storeJob(testJob, true); 47 | jobData = jedis.hgetAll(schema.jobDataMapHashKey(testJob.getKey())); 48 | assertNotNull(jobData); 49 | assertThat(jobData, not(hasKey("timeout"))); 50 | assertThat(jobData, hasKey("foo")); 51 | } 52 | 53 | @Test(expected = ObjectAlreadyExistsException.class) 54 | public void storeJobNoReplace() throws Exception { 55 | jobStore.storeJob(getJobDetail(), false); 56 | jobStore.storeJob(getJobDetail(), false); 57 | } 58 | 59 | @Test 60 | public void storeJobWithReplace() throws Exception { 61 | jobStore.storeJob(getJobDetail(), true); 62 | jobStore.storeJob(getJobDetail(), true); 63 | } 64 | 65 | @Test 66 | public void retrieveJob() throws Exception { 67 | JobDetail testJob = getJobDetail(); 68 | jobStore.storeJob(testJob, false); 69 | 70 | // retrieve the job 71 | JobDetail retrievedJob = jobStore.retrieveJob(testJob.getKey()); 72 | 73 | assertEquals(testJob.getJobClass(), retrievedJob.getJobClass()); 74 | assertEquals(testJob.getDescription(), retrievedJob.getDescription()); 75 | JobDataMap retrievedJobJobDataMap = retrievedJob.getJobDataMap(); 76 | for (String key : testJob.getJobDataMap().keySet()) { 77 | assertThat(retrievedJobJobDataMap, hasKey(key)); 78 | assertEquals(String.valueOf(testJob.getJobDataMap().get(key)), retrievedJobJobDataMap.get(key)); 79 | } 80 | } 81 | 82 | @Test 83 | public void retrieveNonExistentJob() throws Exception { 84 | assertThat(jobStore.retrieveJob(new JobKey("foo", "bar")), nullValue()); 85 | } 86 | 87 | @Test 88 | public void removeJob() throws Exception { 89 | // attempt to remove a non-existent job 90 | assertFalse(jobStore.removeJob(JobKey.jobKey("foo", "bar"))); 91 | 92 | // create and store a job with multiple triggers 93 | JobDetail job = getJobDetail("job1", "jobGroup1"); 94 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "triggerGroup1", job.getKey()); 95 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "triggerGroup1", job.getKey()); 96 | Set triggersSet = new HashSet<>(); 97 | triggersSet.add(trigger1); 98 | triggersSet.add(trigger2); 99 | Map> jobsAndTriggers = new HashMap<>(); 100 | jobsAndTriggers.put(job, triggersSet); 101 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 102 | 103 | assertTrue(jobStore.removeJob(job.getKey())); 104 | 105 | // ensure that the job and all of its triggers were removed 106 | assertThat(jobStore.retrieveJob(job.getKey()), nullValue()); 107 | assertThat(jobStore.retrieveTrigger(trigger1.getKey()), nullValue()); 108 | assertThat(jobStore.retrieveTrigger(trigger2.getKey()), nullValue()); 109 | assertThat(jedis.get(schema.triggerHashKey(trigger1.getKey())), nullValue()); 110 | } 111 | 112 | @Test 113 | public void removeJobs() throws Exception { 114 | // create and store some jobs with triggers 115 | Map> jobsAndTriggers = getJobsAndTriggers(2, 2, 2, 2); 116 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 117 | 118 | List removeKeys = new ArrayList<>(2); 119 | for (JobDetail jobDetail : new ArrayList<>(jobsAndTriggers.keySet()).subList(0, 1)) { 120 | removeKeys.add(jobDetail.getKey()); 121 | } 122 | 123 | assertTrue(jobStore.removeJobs(removeKeys)); 124 | 125 | // ensure that only the proper jobs were removed 126 | for (JobKey removeKey : removeKeys) { 127 | assertThat(jobStore.retrieveJob(removeKey), nullValue()); 128 | } 129 | for (JobDetail jobDetail : new ArrayList<>(jobsAndTriggers.keySet()).subList(1, 3)) { 130 | assertThat(jobStore.retrieveJob(jobDetail.getKey()), not(nullValue())); 131 | } 132 | } 133 | 134 | @Test 135 | public void getNumberOfJobs() throws Exception { 136 | jobStore.storeJob(getJobDetail("job1", "group1"), false); 137 | jobStore.storeJob(getJobDetail("job2", "group1"), false); 138 | jobStore.storeJob(getJobDetail("job3", "group2"), false); 139 | 140 | int numberOfJobs = jobStore.getNumberOfJobs(); 141 | 142 | assertEquals(3, numberOfJobs); 143 | } 144 | 145 | @Test 146 | public void getJobKeys() throws Exception { 147 | jobStore.storeJob(getJobDetail("job1", "group1"), false); 148 | jobStore.storeJob(getJobDetail("job2", "group1"), false); 149 | jobStore.storeJob(getJobDetail("job3", "group2"), false); 150 | 151 | Set jobKeys = jobStore.getJobKeys(GroupMatcher.jobGroupEquals("group1")); 152 | 153 | assertThat(jobKeys, hasSize(2)); 154 | assertThat(jobKeys, containsInAnyOrder(new JobKey("job1", "group1"), new JobKey("job2", "group1"))); 155 | 156 | jobStore.storeJob(getJobDetail("job4", "awesomegroup1"), false); 157 | 158 | jobKeys = jobStore.getJobKeys(GroupMatcher.jobGroupContains("group")); 159 | 160 | assertThat(jobKeys, hasSize(4)); 161 | assertThat(jobKeys, containsInAnyOrder(new JobKey("job1", "group1"), new JobKey("job2", "group1"), 162 | new JobKey("job4", "awesomegroup1"), new JobKey("job3", "group2"))); 163 | 164 | jobKeys = jobStore.getJobKeys(GroupMatcher.jobGroupStartsWith("awe")); 165 | 166 | assertThat(jobKeys, hasSize(1)); 167 | assertThat(jobKeys, containsInAnyOrder(new JobKey("job4", "awesomegroup1"))); 168 | 169 | jobKeys = jobStore.getJobKeys(GroupMatcher.jobGroupEndsWith("1")); 170 | 171 | assertThat(jobKeys, hasSize(3)); 172 | assertThat(jobKeys, containsInAnyOrder(new JobKey("job1", "group1"), new JobKey("job2", "group1"), 173 | new JobKey("job4", "awesomegroup1"))); 174 | } 175 | 176 | @Test 177 | public void getJobGroupNames() throws Exception { 178 | List jobGroupNames = jobStore.getJobGroupNames(); 179 | 180 | assertThat(jobGroupNames, not(nullValue())); 181 | assertThat(jobGroupNames, hasSize(0)); 182 | 183 | jobStore.storeJob(getJobDetail("job1", "group1"), false); 184 | jobStore.storeJob(getJobDetail("job2", "group1"), false); 185 | jobStore.storeJob(getJobDetail("job3", "group2"), false); 186 | 187 | jobGroupNames = jobStore.getJobGroupNames(); 188 | 189 | assertThat(jobGroupNames, hasSize(2)); 190 | assertThat(jobGroupNames, containsInAnyOrder("group1", "group2")); 191 | } 192 | 193 | @Test 194 | public void pauseJob() throws Exception { 195 | // create and store a job with multiple triggers 196 | JobDetail job = getJobDetail("job1", "jobGroup1"); 197 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "triggerGroup1", job.getKey()); 198 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "triggerGroup1", job.getKey()); 199 | Set triggersSet = new HashSet<>(); 200 | triggersSet.add(trigger1); 201 | triggersSet.add(trigger2); 202 | Map> jobsAndTriggers = new HashMap<>(); 203 | jobsAndTriggers.put(job, triggersSet); 204 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 205 | 206 | // pause the job 207 | jobStore.pauseJob(job.getKey()); 208 | 209 | // ensure that the job's triggers were paused 210 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey())); 211 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger2.getKey())); 212 | } 213 | 214 | @Test 215 | public void pauseJobsEquals() throws Exception { 216 | // create and store some jobs with triggers 217 | Map> jobsAndTriggers = getJobsAndTriggers(2, 2, 2, 2); 218 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 219 | 220 | // pause jobs from one of the groups 221 | String pausedGroupName = new ArrayList<>(jobsAndTriggers.keySet()).get(0).getKey().getGroup(); 222 | jobStore.pauseJobs(GroupMatcher.jobGroupEquals(pausedGroupName)); 223 | 224 | // ensure that the appropriate triggers have been paused 225 | for (Map.Entry> entry : jobsAndTriggers.entrySet()) { 226 | for (Trigger trigger : entry.getValue()) { 227 | if(entry.getKey().getKey().getGroup().equals(pausedGroupName)){ 228 | Trigger.TriggerState triggerState = jobStore.getTriggerState(trigger.getKey()); 229 | assertEquals(Trigger.TriggerState.PAUSED, triggerState); 230 | } 231 | else{ 232 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey())); 233 | } 234 | } 235 | } 236 | } 237 | 238 | @Test 239 | public void pauseJobsStartsWith() throws Exception { 240 | JobDetail job1 = getJobDetail("job1", "jobGroup1"); 241 | storeJobAndTriggers(job1, getCronTrigger("trigger1", "triggerGroup1", job1.getKey()), getCronTrigger("trigger2", "triggerGroup1", job1.getKey())); 242 | JobDetail job2 = getJobDetail("job2", "yobGroup1"); 243 | CronTriggerImpl trigger3 = getCronTrigger("trigger3", "triggerGroup3", job2.getKey()); 244 | CronTriggerImpl trigger4 = getCronTrigger("trigger4", "triggerGroup4", job2.getKey()); 245 | storeJobAndTriggers(job2, trigger3, trigger4); 246 | 247 | // pause jobs with groups beginning with "yob" 248 | Collection pausedJobs = jobStore.pauseJobs(GroupMatcher.jobGroupStartsWith("yob")); 249 | assertThat(pausedJobs, hasSize(1)); 250 | assertThat(pausedJobs, containsInAnyOrder("yobGroup1")); 251 | 252 | // ensure that the job was added to the paused jobs set 253 | assertTrue(jedis.sismember(schema.pausedJobGroupsSet(), schema.jobGroupSetKey(job2.getKey()))); 254 | 255 | // ensure that the job's triggers have been paused 256 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger3.getKey())); 257 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger4.getKey())); 258 | } 259 | 260 | @Test 261 | public void pauseJobsEndsWith() throws Exception { 262 | JobDetail job1 = getJobDetail("job1", "jobGroup1"); 263 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "triggerGroup1", job1.getKey()); 264 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "triggerGroup1", job1.getKey()); 265 | storeJobAndTriggers(job1, trigger1, trigger2); 266 | JobDetail job2 = getJobDetail("job2", "yobGroup2"); 267 | CronTriggerImpl trigger3 = getCronTrigger("trigger3", "triggerGroup3", job2.getKey()); 268 | CronTriggerImpl trigger4 = getCronTrigger("trigger4", "triggerGroup4", job2.getKey()); 269 | storeJobAndTriggers(job2, trigger3, trigger4); 270 | 271 | // pause job groups ending with "1" 272 | Collection pausedJobs = jobStore.pauseJobs(GroupMatcher.jobGroupEndsWith("1")); 273 | assertThat(pausedJobs, hasSize(1)); 274 | assertThat(pausedJobs, containsInAnyOrder("jobGroup1")); 275 | 276 | // ensure that the job was added to the paused jobs set 277 | assertTrue(jedis.sismember(schema.pausedJobGroupsSet(), schema.jobGroupSetKey(job1.getKey()))); 278 | 279 | // ensure that the job's triggers have been paused 280 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey())); 281 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger2.getKey())); 282 | } 283 | 284 | @Test 285 | public void pauseJobsContains() throws Exception { 286 | JobDetail job1 = getJobDetail("job1", "jobGroup1"); 287 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "triggerGroup1", job1.getKey()); 288 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "triggerGroup1", job1.getKey()); 289 | storeJobAndTriggers(job1, trigger1, trigger2); 290 | JobDetail job2 = getJobDetail("job2", "yobGroup2"); 291 | CronTriggerImpl trigger3 = getCronTrigger("trigger3", "triggerGroup3", job2.getKey()); 292 | CronTriggerImpl trigger4 = getCronTrigger("trigger4", "triggerGroup4", job2.getKey()); 293 | storeJobAndTriggers(job2, trigger3, trigger4); 294 | 295 | // Pause job groups containing "foo". Should result in no jobs being paused. 296 | Collection pausedJobs = jobStore.pauseJobs(GroupMatcher.jobGroupContains("foo")); 297 | assertThat(pausedJobs, hasSize(0)); 298 | 299 | // pause jobs containing "Group" 300 | pausedJobs = jobStore.pauseJobs(GroupMatcher.jobGroupContains("Group")); 301 | assertThat(pausedJobs, hasSize(2)); 302 | assertThat(pausedJobs, containsInAnyOrder("jobGroup1", "yobGroup2")); 303 | 304 | // ensure that both jobs were added to the paused jobs set 305 | assertTrue(jedis.sismember(schema.pausedJobGroupsSet(), schema.jobGroupSetKey(job1.getKey()))); 306 | assertTrue(jedis.sismember(schema.pausedJobGroupsSet(), schema.jobGroupSetKey(job2.getKey()))); 307 | 308 | // ensure that all triggers were paused 309 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey())); 310 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger2.getKey())); 311 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger3.getKey())); 312 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger4.getKey())); 313 | } 314 | 315 | @Test 316 | public void resumeJob() throws Exception { 317 | // create and store a job with multiple triggers 318 | JobDetail job = getJobDetail("job1", "jobGroup1"); 319 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "triggerGroup1", job.getKey()); 320 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "triggerGroup1", job.getKey()); 321 | storeJobAndTriggers(job, trigger1, trigger2); 322 | 323 | // pause the job 324 | jobStore.pauseJob(job.getKey()); 325 | 326 | // ensure that the job's triggers have been paused 327 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey())); 328 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger2.getKey())); 329 | 330 | // resume the job 331 | jobStore.resumeJob(job.getKey()); 332 | 333 | // ensure that the triggers have been resumed 334 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger1.getKey())); 335 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger2.getKey())); 336 | } 337 | 338 | @Test 339 | public void resumeJobsEquals() throws Exception { 340 | // attempt to resume jobs for a non-existent job group 341 | Collection resumedJobGroups = jobStore.resumeJobs(GroupMatcher.jobGroupEquals("foobar")); 342 | assertThat(resumedJobGroups, hasSize(0)); 343 | 344 | // store some jobs with triggers 345 | Map> jobsAndTriggers = getJobsAndTriggers(2, 2, 2, 2); 346 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 347 | 348 | // pause one of the job groups 349 | String pausedGroupName = new ArrayList<>(jobsAndTriggers.keySet()).get(0).getKey().getGroup(); 350 | jobStore.pauseJobs(GroupMatcher.jobGroupEquals(pausedGroupName)); 351 | 352 | // ensure that the appropriate triggers have been paused 353 | for (Map.Entry> entry : jobsAndTriggers.entrySet()) { 354 | for (Trigger trigger : entry.getValue()) { 355 | if(entry.getKey().getKey().getGroup().equals(pausedGroupName)){ 356 | Trigger.TriggerState triggerState = jobStore.getTriggerState(trigger.getKey()); 357 | assertEquals(Trigger.TriggerState.PAUSED, triggerState); 358 | } 359 | else{ 360 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey())); 361 | } 362 | } 363 | } 364 | 365 | // resume the paused jobs 366 | resumedJobGroups = jobStore.resumeJobs(GroupMatcher.jobGroupEquals(pausedGroupName)); 367 | 368 | assertThat(resumedJobGroups, hasSize(1)); 369 | assertEquals(pausedGroupName, new ArrayList<>(resumedJobGroups).get(0)); 370 | 371 | for (Trigger trigger : new ArrayList<>(jobsAndTriggers.entrySet()).get(0).getValue()) { 372 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey())); 373 | } 374 | } 375 | 376 | @Test 377 | public void resumeJobsEndsWith() throws Exception { 378 | Map> jobsAndTriggers = getJobsAndTriggers(2, 2, 2, 2); 379 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 380 | 381 | // pause one of the job groups 382 | String pausedGroupName = new ArrayList<>(jobsAndTriggers.keySet()).get(0).getKey().getGroup(); 383 | String substring = pausedGroupName.substring(pausedGroupName.length() - 1, pausedGroupName.length()); 384 | Collection pausedGroups = jobStore.pauseJobs(GroupMatcher.jobGroupEndsWith(substring)); 385 | 386 | assertThat(pausedGroups, hasSize(1)); 387 | assertThat(pausedGroups, containsInAnyOrder(pausedGroupName)); 388 | 389 | // resume the paused jobs 390 | Collection resumedGroups = jobStore.resumeJobs(GroupMatcher.jobGroupEndsWith(substring)); 391 | 392 | assertThat(resumedGroups, hasSize(1)); 393 | assertThat(resumedGroups, containsInAnyOrder(pausedGroupName)); 394 | 395 | for (Trigger trigger : new ArrayList<>(jobsAndTriggers.entrySet()).get(0).getValue()) { 396 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey())); 397 | } 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/StoreTriggerTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import net.joelinn.quartz.jobstore.RedisJobStoreSchema; 5 | import net.joelinn.quartz.jobstore.AbstractRedisStorage; 6 | import net.joelinn.quartz.jobstore.RedisStorage; 7 | import net.joelinn.quartz.jobstore.RedisTriggerState; 8 | import org.hamcrest.MatcherAssert; 9 | import org.junit.Test; 10 | import org.quartz.*; 11 | import org.quartz.impl.calendar.WeeklyCalendar; 12 | import org.quartz.impl.matchers.GroupMatcher; 13 | import org.quartz.impl.triggers.CronTriggerImpl; 14 | import org.quartz.spi.OperableTrigger; 15 | import org.quartz.spi.SchedulerSignaler; 16 | import org.quartz.spi.TriggerFiredResult; 17 | 18 | import java.util.*; 19 | 20 | import static org.hamcrest.CoreMatchers.equalTo; 21 | import static org.hamcrest.CoreMatchers.instanceOf; 22 | import static org.hamcrest.CoreMatchers.not; 23 | import static org.hamcrest.MatcherAssert.assertThat; 24 | import static org.hamcrest.Matchers.containsInAnyOrder; 25 | import static org.hamcrest.Matchers.notNullValue; 26 | import static org.hamcrest.Matchers.nullValue; 27 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 28 | import static org.hamcrest.collection.IsMapContaining.hasKey; 29 | import static org.junit.Assert.*; 30 | import static org.mockito.Mockito.mock; 31 | 32 | /** 33 | * Joe Linn 34 | * 7/15/2014 35 | */ 36 | public class StoreTriggerTest extends BaseTest{ 37 | 38 | @Test 39 | public void storeTrigger() throws Exception { 40 | CronTriggerImpl trigger = getCronTrigger(); 41 | trigger.getJobDataMap().put("foo", "bar"); 42 | 43 | jobStore.storeTrigger(trigger, false); 44 | 45 | final String triggerHashKey = schema.triggerHashKey(trigger.getKey()); 46 | Map triggerMap = jedis.hgetAll(triggerHashKey); 47 | assertThat(triggerMap, hasKey("description")); 48 | assertEquals(trigger.getDescription(), triggerMap.get("description")); 49 | assertThat(triggerMap, hasKey("trigger_class")); 50 | assertEquals(trigger.getClass().getName(), triggerMap.get("trigger_class")); 51 | 52 | assertTrue("The trigger hash key is not a member of the triggers set.", jedis.sismember(schema.triggersSet(), triggerHashKey)); 53 | assertTrue("The trigger group set key is not a member of the trigger group set.", jedis.sismember(schema.triggerGroupsSet(), schema.triggerGroupSetKey(trigger.getKey()))); 54 | assertTrue(jedis.sismember(schema.triggerGroupSetKey(trigger.getKey()), triggerHashKey)); 55 | assertTrue(jedis.sismember(schema.jobTriggersSetKey(trigger.getJobKey()), triggerHashKey)); 56 | String triggerDataMapHashKey = schema.triggerDataMapHashKey(trigger.getKey()); 57 | MatcherAssert.assertThat(jedis.exists(triggerDataMapHashKey), equalTo(true)); 58 | MatcherAssert.assertThat(jedis.hget(triggerDataMapHashKey, "foo"), equalTo("bar")); 59 | } 60 | 61 | @Test(expected = JobPersistenceException.class) 62 | public void storeTriggerNoReplace() throws Exception { 63 | jobStore.storeTrigger(getCronTrigger(), false); 64 | jobStore.storeTrigger(getCronTrigger(), false); 65 | } 66 | 67 | @Test 68 | public void storeTriggerWithReplace() throws Exception { 69 | jobStore.storeTrigger(getCronTrigger(), true); 70 | jobStore.storeTrigger(getCronTrigger(), true); 71 | } 72 | 73 | @Test 74 | public void retrieveTrigger() throws Exception { 75 | CronTriggerImpl cronTrigger = getCronTrigger(); 76 | jobStore.storeJob(getJobDetail(), false); 77 | jobStore.storeTrigger(cronTrigger, false); 78 | 79 | OperableTrigger operableTrigger = jobStore.retrieveTrigger(cronTrigger.getKey()); 80 | 81 | assertThat(operableTrigger, instanceOf(CronTriggerImpl.class)); 82 | assertThat(operableTrigger.getFireInstanceId(), notNullValue()); 83 | CronTriggerImpl retrievedTrigger = (CronTriggerImpl) operableTrigger; 84 | 85 | assertEquals(cronTrigger.getCronExpression(), retrievedTrigger.getCronExpression()); 86 | assertEquals(cronTrigger.getTimeZone(), retrievedTrigger.getTimeZone()); 87 | assertEquals(cronTrigger.getStartTime(), retrievedTrigger.getStartTime()); 88 | } 89 | 90 | @Test 91 | public void removeTrigger() throws Exception { 92 | JobDetail job = getJobDetail(); 93 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "triggerGroup", job.getKey()); 94 | trigger1.getJobDataMap().put("foo", "bar"); 95 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "triggerGroup", job.getKey()); 96 | 97 | jobStore.storeJob(job, false); 98 | jobStore.storeTrigger(trigger1, false); 99 | jobStore.storeTrigger(trigger2, false); 100 | 101 | jobStore.removeTrigger(trigger1.getKey()); 102 | 103 | // ensure that the trigger was removed, but the job was not 104 | assertThat(jobStore.retrieveTrigger(trigger1.getKey()), nullValue()); 105 | assertThat(jobStore.retrieveJob(job.getKey()), not(nullValue())); 106 | 107 | // remove the second trigger 108 | jobStore.removeTrigger(trigger2.getKey()); 109 | 110 | // ensure that both the trigger and job were removed 111 | assertThat(jobStore.retrieveTrigger(trigger2.getKey()), nullValue()); 112 | assertThat(jobStore.retrieveJob(job.getKey()), nullValue()); 113 | MatcherAssert.assertThat(jedis.exists(schema.triggerDataMapHashKey(trigger1.getKey())), equalTo(false)); 114 | } 115 | 116 | @Test 117 | public void getTriggersForJob() throws Exception { 118 | JobDetail job = getJobDetail(); 119 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "triggerGroup", job.getKey()); 120 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "triggerGroup", job.getKey()); 121 | 122 | jobStore.storeJob(job, false); 123 | jobStore.storeTrigger(trigger1, false); 124 | jobStore.storeTrigger(trigger2, false); 125 | 126 | List triggers = jobStore.getTriggersForJob(job.getKey()); 127 | assertThat(triggers, hasSize(2)); 128 | } 129 | 130 | @Test 131 | public void getNumberOfTriggers() throws Exception { 132 | JobDetail job = getJobDetail(); 133 | jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false); 134 | jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false); 135 | jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false); 136 | jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false); 137 | 138 | int numberOfTriggers = jobStore.getNumberOfTriggers(); 139 | 140 | assertEquals(4, numberOfTriggers); 141 | } 142 | 143 | @Test 144 | public void getTriggerKeys() throws Exception { 145 | JobDetail job = getJobDetail(); 146 | jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false); 147 | jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false); 148 | jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false); 149 | jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false); 150 | 151 | Set triggerKeys = jobStore.getTriggerKeys(GroupMatcher.triggerGroupEquals("group1")); 152 | 153 | assertThat(triggerKeys, hasSize(2)); 154 | assertThat(triggerKeys, containsInAnyOrder(new TriggerKey("trigger2", "group1"), new TriggerKey("trigger1", "group1"))); 155 | 156 | jobStore.storeTrigger(getCronTrigger("trigger4", "triggergroup1", job.getKey()), false); 157 | 158 | triggerKeys = jobStore.getTriggerKeys(GroupMatcher.triggerGroupContains("group")); 159 | 160 | assertThat(triggerKeys, hasSize(5)); 161 | 162 | triggerKeys = jobStore.getTriggerKeys(GroupMatcher.triggerGroupEndsWith("1")); 163 | 164 | assertThat(triggerKeys, hasSize(3)); 165 | assertThat(triggerKeys, containsInAnyOrder(new TriggerKey("trigger2", "group1"), 166 | new TriggerKey("trigger1", "group1"), new TriggerKey("trigger4", "triggergroup1"))); 167 | 168 | triggerKeys = jobStore.getTriggerKeys(GroupMatcher.triggerGroupStartsWith("trig")); 169 | 170 | assertThat(triggerKeys, hasSize(1)); 171 | assertThat(triggerKeys, containsInAnyOrder(new TriggerKey("trigger4", "triggergroup1"))); 172 | } 173 | 174 | @Test 175 | public void getTriggerGroupNames() throws Exception { 176 | List triggerGroupNames = jobStore.getTriggerGroupNames(); 177 | 178 | assertThat(triggerGroupNames, not(nullValue())); 179 | assertThat(triggerGroupNames, hasSize(0)); 180 | 181 | JobDetail job = getJobDetail(); 182 | jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false); 183 | jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false); 184 | jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false); 185 | jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false); 186 | 187 | triggerGroupNames = jobStore.getTriggerGroupNames(); 188 | 189 | assertThat(triggerGroupNames, hasSize(3)); 190 | assertThat(triggerGroupNames, containsInAnyOrder("group3", "group2", "group1")); 191 | } 192 | 193 | @Test 194 | public void getTriggerState() throws Exception { 195 | SchedulerSignaler signaler = mock(SchedulerSignaler.class); 196 | AbstractRedisStorage storageDriver = new RedisStorage(new RedisJobStoreSchema(), new ObjectMapper(), signaler, "scheduler1", 2000); 197 | 198 | // attempt to retrieve the state of a non-existent trigger 199 | Trigger.TriggerState state = jobStore.getTriggerState(new TriggerKey("foobar")); 200 | assertEquals(Trigger.TriggerState.NONE, state); 201 | 202 | // store a trigger 203 | JobDetail job = getJobDetail(); 204 | CronTriggerImpl cronTrigger = getCronTrigger("trigger1", "group1", job.getKey()); 205 | jobStore.storeTrigger(cronTrigger, false); 206 | 207 | // the newly-stored trigger's state should be NONE 208 | state = jobStore.getTriggerState(cronTrigger.getKey()); 209 | assertEquals(Trigger.TriggerState.NORMAL, state); 210 | 211 | // set the trigger's state 212 | storageDriver.setTriggerState(RedisTriggerState.WAITING, 500, schema.triggerHashKey(cronTrigger.getKey()), jedis); 213 | 214 | // the trigger's state should now be NORMAL 215 | state = jobStore.getTriggerState(cronTrigger.getKey()); 216 | assertEquals(Trigger.TriggerState.NORMAL, state); 217 | } 218 | 219 | @Test 220 | public void pauseTrigger() throws Exception { 221 | SchedulerSignaler signaler = mock(SchedulerSignaler.class); 222 | AbstractRedisStorage storageDriver = new RedisStorage(new RedisJobStoreSchema(), new ObjectMapper(), signaler, "scheduler1", 2000); 223 | 224 | // store a trigger 225 | JobDetail job = getJobDetail(); 226 | CronTriggerImpl cronTrigger = getCronTrigger("trigger1", "group1", job.getKey()); 227 | cronTrigger.setNextFireTime(new Date(System.currentTimeMillis())); 228 | jobStore.storeTrigger(cronTrigger, false); 229 | 230 | // set the trigger's state to COMPLETED 231 | storageDriver.setTriggerState(RedisTriggerState.COMPLETED, 500, schema.triggerHashKey(cronTrigger.getKey()), jedis); 232 | jobStore.pauseTrigger(cronTrigger.getKey()); 233 | 234 | // trigger's state should not have changed 235 | assertEquals(Trigger.TriggerState.COMPLETE, jobStore.getTriggerState(cronTrigger.getKey())); 236 | 237 | // set the trigger's state to BLOCKED 238 | storageDriver.setTriggerState(RedisTriggerState.BLOCKED, 500, schema.triggerHashKey(cronTrigger.getKey()), jedis); 239 | jobStore.pauseTrigger(cronTrigger.getKey()); 240 | 241 | // trigger's state should be PAUSED 242 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(cronTrigger.getKey())); 243 | 244 | // set the trigger's state to ACQUIRED 245 | storageDriver.setTriggerState(RedisTriggerState.ACQUIRED, 500, schema.triggerHashKey(cronTrigger.getKey()), jedis); 246 | jobStore.pauseTrigger(cronTrigger.getKey()); 247 | 248 | // trigger's state should be PAUSED 249 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(cronTrigger.getKey())); 250 | } 251 | 252 | @Test 253 | public void pauseTriggersEquals() throws Exception { 254 | // store triggers 255 | JobDetail job = getJobDetail(); 256 | jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false); 257 | jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false); 258 | jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false); 259 | jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false); 260 | 261 | // pause triggers 262 | Collection pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEquals("group1")); 263 | 264 | assertThat(pausedGroups, hasSize(1)); 265 | assertThat(pausedGroups, containsInAnyOrder("group1")); 266 | 267 | // ensure that the triggers were actually paused 268 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(new TriggerKey("trigger1", "group1"))); 269 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(new TriggerKey("trigger2", "group1"))); 270 | } 271 | 272 | @Test 273 | public void pauseTriggersStartsWith() throws Exception { 274 | JobDetail job = getJobDetail(); 275 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey()); 276 | CronTriggerImpl trigger2 = getCronTrigger("trigger1", "group2", job.getKey()); 277 | CronTriggerImpl trigger3 = getCronTrigger("trigger1", "foogroup1", job.getKey()); 278 | storeJobAndTriggers(job, trigger1, trigger2, trigger3); 279 | 280 | Collection pausedTriggerGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupStartsWith("group")); 281 | 282 | assertThat(pausedTriggerGroups, hasSize(2)); 283 | assertThat(pausedTriggerGroups, containsInAnyOrder("group1", "group2")); 284 | 285 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey())); 286 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger2.getKey())); 287 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger3.getKey())); 288 | } 289 | 290 | @Test 291 | public void pauseTriggersEndsWith() throws Exception { 292 | JobDetail job = getJobDetail(); 293 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey()); 294 | CronTriggerImpl trigger2 = getCronTrigger("trigger1", "group2", job.getKey()); 295 | CronTriggerImpl trigger3 = getCronTrigger("trigger1", "foogroup1", job.getKey()); 296 | storeJobAndTriggers(job, trigger1, trigger2, trigger3); 297 | 298 | Collection pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEndsWith("oup1")); 299 | 300 | assertThat(pausedGroups, hasSize(2)); 301 | assertThat(pausedGroups, containsInAnyOrder("group1", "foogroup1")); 302 | 303 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey())); 304 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger2.getKey())); 305 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger3.getKey())); 306 | } 307 | 308 | @Test 309 | public void resumeTrigger() throws Exception { 310 | // create and store a job and trigger 311 | JobDetail job = getJobDetail(); 312 | jobStore.storeJob(job, false); 313 | CronTriggerImpl trigger = getCronTrigger("trigger1", "group1", job.getKey()); 314 | trigger.computeFirstFireTime(new WeeklyCalendar()); 315 | jobStore.storeTrigger(trigger, false); 316 | 317 | // pause the trigger 318 | jobStore.pauseTrigger(trigger.getKey()); 319 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger.getKey())); 320 | 321 | // resume the trigger 322 | jobStore.resumeTrigger(trigger.getKey()); 323 | // the trigger state should now be NORMAL 324 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey())); 325 | 326 | // attempt to resume the trigger, again 327 | jobStore.resumeTrigger(trigger.getKey()); 328 | // the trigger state should not have changed 329 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey())); 330 | } 331 | 332 | @Test 333 | public void resumeTriggersEquals() throws Exception { 334 | // store triggers and job 335 | JobDetail job = getJobDetail(); 336 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey()); 337 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey()); 338 | CronTriggerImpl trigger3 = getCronTrigger("trigger3", "group2", job.getKey()); 339 | CronTriggerImpl trigger4 = getCronTrigger("trigger4", "group3", job.getKey()); 340 | storeJobAndTriggers(job, trigger1, trigger2, trigger3, trigger4); 341 | 342 | // pause triggers 343 | Collection pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEquals("group1")); 344 | 345 | assertThat(pausedGroups, hasSize(1)); 346 | assertThat(pausedGroups, containsInAnyOrder("group1")); 347 | 348 | // ensure that the triggers were actually paused 349 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(new TriggerKey("trigger1", "group1"))); 350 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(new TriggerKey("trigger2", "group1"))); 351 | 352 | // resume triggers 353 | Collection resumedGroups = jobStore.resumeTriggers(GroupMatcher.triggerGroupEquals("group1")); 354 | 355 | assertThat(resumedGroups, hasSize(1)); 356 | assertThat(resumedGroups, containsInAnyOrder("group1")); 357 | 358 | // ensure that the triggers were resumed 359 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(new TriggerKey("trigger1", "group1"))); 360 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(new TriggerKey("trigger2", "group1"))); 361 | } 362 | 363 | @Test 364 | public void resumeTriggersEndsWith() throws Exception { 365 | JobDetail job = getJobDetail(); 366 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey()); 367 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey()); 368 | CronTriggerImpl trigger3 = getCronTrigger("trigger3", "group2", job.getKey()); 369 | CronTriggerImpl trigger4 = getCronTrigger("trigger4", "group3", job.getKey()); 370 | storeJobAndTriggers(job, trigger1, trigger2, trigger3, trigger4); 371 | 372 | Collection pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEndsWith("1")); 373 | 374 | assertThat(pausedGroups, hasSize(1)); 375 | assertThat(pausedGroups, containsInAnyOrder("group1")); 376 | 377 | // ensure that the triggers were actually paused 378 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey())); 379 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger2.getKey())); 380 | 381 | // resume triggers 382 | Collection resumedGroups = jobStore.resumeTriggers(GroupMatcher.triggerGroupEndsWith("1")); 383 | 384 | assertThat(resumedGroups, hasSize(1)); 385 | assertThat(resumedGroups, containsInAnyOrder("group1")); 386 | 387 | // ensure that the triggers were actually resumed 388 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger1.getKey())); 389 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger2.getKey())); 390 | } 391 | 392 | @Test 393 | public void resumeTriggersStartsWith() throws Exception { 394 | JobDetail job = getJobDetail(); 395 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "mygroup1", job.getKey()); 396 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey()); 397 | CronTriggerImpl trigger3 = getCronTrigger("trigger3", "group2", job.getKey()); 398 | CronTriggerImpl trigger4 = getCronTrigger("trigger4", "group3", job.getKey()); 399 | storeJobAndTriggers(job, trigger1, trigger2, trigger3, trigger4); 400 | 401 | Collection pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupStartsWith("my")); 402 | 403 | assertThat(pausedGroups, hasSize(1)); 404 | assertThat(pausedGroups, containsInAnyOrder("mygroup1")); 405 | 406 | // ensure that the triggers were actually paused 407 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger1.getKey())); 408 | 409 | // resume triggers 410 | Collection resumedGroups = jobStore.resumeTriggers(GroupMatcher.triggerGroupStartsWith("my")); 411 | 412 | assertThat(resumedGroups, hasSize(1)); 413 | assertThat(resumedGroups, containsInAnyOrder("mygroup1")); 414 | 415 | // ensure that the triggers were actually resumed 416 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger1.getKey())); 417 | } 418 | 419 | @Test 420 | public void getPausedTriggerGroups() throws Exception { 421 | // store triggers 422 | JobDetail job = getJobDetail(); 423 | jobStore.storeTrigger(getCronTrigger("trigger1", "group1", job.getKey()), false); 424 | jobStore.storeTrigger(getCronTrigger("trigger2", "group1", job.getKey()), false); 425 | jobStore.storeTrigger(getCronTrigger("trigger3", "group2", job.getKey()), false); 426 | jobStore.storeTrigger(getCronTrigger("trigger4", "group3", job.getKey()), false); 427 | 428 | // pause triggers 429 | Collection pausedGroups = jobStore.pauseTriggers(GroupMatcher.triggerGroupEquals("group1")); 430 | pausedGroups.addAll(jobStore.pauseTriggers(GroupMatcher.triggerGroupEquals("group3"))); 431 | 432 | assertThat(pausedGroups, hasSize(2)); 433 | assertThat(pausedGroups, containsInAnyOrder("group3", "group1")); 434 | 435 | // retrieve paused trigger groups 436 | Set pausedTriggerGroups = jobStore.getPausedTriggerGroups(); 437 | assertThat(pausedTriggerGroups, hasSize(2)); 438 | assertThat(pausedTriggerGroups, containsInAnyOrder("group1", "group3")); 439 | } 440 | 441 | @Test 442 | public void pauseAndResumeAll() throws Exception { 443 | // store some jobs with triggers 444 | Map> jobsAndTriggers = getJobsAndTriggers(2, 2, 2, 2); 445 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 446 | 447 | // ensure that all triggers are in the NORMAL state 448 | for (Map.Entry> jobDetailSetEntry : jobsAndTriggers.entrySet()) { 449 | for (Trigger trigger : jobDetailSetEntry.getValue()) { 450 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey())); 451 | } 452 | } 453 | 454 | jobStore.pauseAll(); 455 | 456 | // ensure that all triggers were paused 457 | for (Map.Entry> jobDetailSetEntry : jobsAndTriggers.entrySet()) { 458 | for (Trigger trigger : jobDetailSetEntry.getValue()) { 459 | assertEquals(Trigger.TriggerState.PAUSED, jobStore.getTriggerState(trigger.getKey())); 460 | } 461 | } 462 | 463 | // resume all triggers 464 | jobStore.resumeAll(); 465 | 466 | // ensure that all triggers are again in the NORMAL state 467 | for (Map.Entry> jobDetailSetEntry : jobsAndTriggers.entrySet()) { 468 | for (Trigger trigger : jobDetailSetEntry.getValue()) { 469 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey())); 470 | } 471 | } 472 | } 473 | 474 | @Test 475 | @SuppressWarnings("unchecked") 476 | public void triggersFired() throws Exception { 477 | // store some jobs with triggers 478 | Map> jobsAndTriggers = getJobsAndTriggers(2, 2, 2, 2, "* * * * * ?"); 479 | 480 | // disallow concurrent execution for one of the jobs 481 | Map.Entry> firstEntry = jobsAndTriggers.entrySet().iterator().next(); 482 | JobDetail nonConcurrentKey = firstEntry.getKey().getJobBuilder().ofType(TestJobNonConcurrent.class).build(); 483 | Set nonConcurrentTriggers = firstEntry.getValue(); 484 | jobsAndTriggers.remove(firstEntry.getKey()); 485 | jobsAndTriggers.put(nonConcurrentKey, nonConcurrentTriggers); 486 | 487 | jobStore.storeCalendar("testCalendar", new WeeklyCalendar(), false, true); 488 | jobStore.storeJobsAndTriggers(jobsAndTriggers, false); 489 | 490 | List acquiredTriggers = jobStore.acquireNextTriggers(System.currentTimeMillis() - 1000, 500, 4000); 491 | assertThat(acquiredTriggers, hasSize(13)); 492 | 493 | int lockedTriggers = 0; 494 | // ensure that all triggers are in the NORMAL state and have been ACQUIRED 495 | for (Map.Entry> jobDetailSetEntry : jobsAndTriggers.entrySet()) { 496 | for (Trigger trigger : jobDetailSetEntry.getValue()) { 497 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger.getKey())); 498 | String triggerHashKey = schema.triggerHashKey(trigger.getKey()); 499 | if (jobDetailSetEntry.getKey().isConcurrentExectionDisallowed()) { 500 | if (jedis.zscore(schema.triggerStateKey(RedisTriggerState.ACQUIRED), triggerHashKey) != null) { 501 | assertThat("acquired trigger should be locked", jedis.get(schema.triggerLockKey(schema.triggerKey(triggerHashKey))), notNullValue()); 502 | lockedTriggers++; 503 | } else { 504 | assertThat("non-acquired trigger should not be locked", jedis.get(schema.triggerLockKey(schema.triggerKey(triggerHashKey))), nullValue()); 505 | } 506 | } else { 507 | assertThat(jedis.zscore(schema.triggerStateKey(RedisTriggerState.ACQUIRED), triggerHashKey), not(nullValue())); 508 | } 509 | } 510 | } 511 | 512 | assertThat(lockedTriggers, equalTo(1)); 513 | 514 | Set triggers = (Set) new ArrayList<>(jobsAndTriggers.entrySet()).get(0).getValue(); 515 | List triggerFiredResults = jobStore.triggersFired(new ArrayList<>(triggers)); 516 | assertThat("exactly one trigger fired for job with concurrent execution disallowed", triggerFiredResults, hasSize(1)); 517 | 518 | triggers = (Set) new ArrayList<>(jobsAndTriggers.entrySet()).get(1).getValue(); 519 | triggerFiredResults = jobStore.triggersFired(new ArrayList<>(triggers)); 520 | assertThat("all triggers fired for job with concurrent execution allowed", triggerFiredResults, hasSize(4)); 521 | } 522 | 523 | @Test 524 | public void replaceTrigger() throws Exception { 525 | assertFalse(jobStore.replaceTrigger(TriggerKey.triggerKey("foo", "bar"), getCronTrigger())); 526 | 527 | // store triggers and job 528 | JobDetail job = getJobDetail(); 529 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey()); 530 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey()); 531 | storeJobAndTriggers(job, trigger1, trigger2); 532 | 533 | CronTriggerImpl newTrigger = getCronTrigger("newTrigger", "group1", job.getKey()); 534 | 535 | assertTrue(jobStore.replaceTrigger(trigger1.getKey(), newTrigger)); 536 | 537 | // ensure that the proper trigger was replaced 538 | assertThat(jobStore.retrieveTrigger(trigger1.getKey()), nullValue()); 539 | 540 | List jobTriggers = jobStore.getTriggersForJob(job.getKey()); 541 | 542 | assertThat(jobTriggers, hasSize(2)); 543 | List jobTriggerKeys = new ArrayList<>(jobTriggers.size()); 544 | for (OperableTrigger jobTrigger : jobTriggers) { 545 | jobTriggerKeys.add(jobTrigger.getKey()); 546 | } 547 | 548 | assertThat(jobTriggerKeys, containsInAnyOrder(trigger2.getKey(), newTrigger.getKey())); 549 | } 550 | 551 | @Test 552 | public void replaceTriggerSingleTriggerNonDurableJob() throws Exception { 553 | // store trigger and job 554 | JobDetail job = getJobDetail(); 555 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey()); 556 | storeJobAndTriggers(job, trigger1); 557 | 558 | CronTriggerImpl newTrigger = getCronTrigger("newTrigger", "group1", job.getKey()); 559 | 560 | assertTrue(jobStore.replaceTrigger(trigger1.getKey(), newTrigger)); 561 | 562 | // ensure that the proper trigger was replaced 563 | assertThat(jobStore.retrieveTrigger(trigger1.getKey()), nullValue()); 564 | 565 | List jobTriggers = jobStore.getTriggersForJob(job.getKey()); 566 | 567 | assertThat(jobTriggers, hasSize(1)); 568 | 569 | // ensure that the job still exists 570 | assertThat(jobStore.retrieveJob(job.getKey()), not(nullValue())); 571 | } 572 | 573 | @Test(expected = JobPersistenceException.class) 574 | public void replaceTriggerWithDifferentJob() throws Exception { 575 | // store triggers and job 576 | JobDetail job = getJobDetail(); 577 | jobStore.storeJob(job, false); 578 | CronTriggerImpl trigger1 = getCronTrigger("trigger1", "group1", job.getKey()); 579 | jobStore.storeTrigger(trigger1, false); 580 | CronTriggerImpl trigger2 = getCronTrigger("trigger2", "group1", job.getKey()); 581 | jobStore.storeTrigger(trigger2, false); 582 | 583 | CronTriggerImpl newTrigger = getCronTrigger("newTrigger", "group1", JobKey.jobKey("foo", "bar")); 584 | 585 | jobStore.replaceTrigger(trigger1.getKey(), newTrigger); 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/TestJob.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import org.quartz.Job; 4 | import org.quartz.JobExecutionContext; 5 | import org.quartz.JobExecutionException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | /** 10 | * Joe Linn 11 | * 7/15/2014 12 | */ 13 | public class TestJob implements Job{ 14 | private static Logger logger = LoggerFactory.getLogger(TestJob.class); 15 | 16 | protected int timeout; 17 | 18 | public void setTimeout(int timeout) { 19 | this.timeout = timeout; 20 | } 21 | 22 | @Override 23 | public void execute(JobExecutionContext context) throws JobExecutionException { 24 | logger.info(String.format("Test job running with timeout %s", timeout)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/TestJobNonConcurrent.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import org.quartz.DisallowConcurrentExecution; 4 | 5 | /** 6 | * User: Joe Linn 7 | * Date: 7/23/2014 8 | * Time: 11:10 AM 9 | */ 10 | @DisallowConcurrentExecution 11 | public class TestJobNonConcurrent extends TestJob{ 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/TestJobPersist.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import org.quartz.PersistJobDataAfterExecution; 4 | 5 | /** 6 | * Joe Linn 7 | * 7/22/2014 8 | */ 9 | @PersistJobDataAfterExecution 10 | public class TestJobPersist extends TestJob{ 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/TestUtils.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import org.quartz.*; 4 | 5 | import java.io.IOException; 6 | import java.net.ServerSocket; 7 | 8 | /** 9 | * @author Joe Linn 10 | * 10/4/2016 11 | */ 12 | public class TestUtils { 13 | private TestUtils() {} 14 | 15 | public static int getPort() throws IOException { 16 | try (ServerSocket socket = new ServerSocket(0)) { 17 | socket.setReuseAddress(true); 18 | return socket.getLocalPort(); 19 | } 20 | } 21 | 22 | 23 | public static JobDetail createJob(Class jobClass, String name, String group) { 24 | return JobBuilder.newJob(jobClass) 25 | .withIdentity(name, group) 26 | .build(); 27 | } 28 | 29 | 30 | public static CronTrigger createCronTrigger(String name, String group, String cron) { 31 | return TriggerBuilder.newTrigger() 32 | .withIdentity(name, group) 33 | .withSchedule(CronScheduleBuilder.cronSchedule(cron)) 34 | .build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/TriggeredJobCompleteTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.quartz.JobBuilder; 6 | import org.quartz.JobDetail; 7 | import org.quartz.JobPersistenceException; 8 | import org.quartz.Trigger; 9 | import org.quartz.impl.triggers.CronTriggerImpl; 10 | 11 | import static org.hamcrest.CoreMatchers.not; 12 | import static org.hamcrest.CoreMatchers.nullValue; 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.junit.Assert.assertEquals; 15 | import static org.junit.Assert.assertFalse; 16 | import static org.mockito.Mockito.verify; 17 | 18 | /** 19 | * Joe Linn 20 | * 7/22/2014 21 | */ 22 | public class TriggeredJobCompleteTest extends BaseTest{ 23 | protected JobDetail job; 24 | 25 | protected CronTriggerImpl trigger1; 26 | 27 | protected CronTriggerImpl trigger2; 28 | 29 | @Before 30 | public void setUp() throws JobPersistenceException { 31 | // store a job with some triggers 32 | job = getJobDetail("job1", "jobGroup1"); 33 | trigger1 = getCronTrigger("trigger1", "triggerGroup1", job.getKey()); 34 | trigger2 = getCronTrigger("trigger2", "triggerGroup1", job.getKey()); 35 | storeJobAndTriggers(job, trigger1, trigger2); 36 | } 37 | 38 | @Test 39 | public void triggeredJobCompleteDelete() throws JobPersistenceException { 40 | jobStore.triggeredJobComplete(trigger1, job, Trigger.CompletedExecutionInstruction.DELETE_TRIGGER); 41 | 42 | // ensure that the proper trigger was deleted 43 | assertThat(jobStore.retrieveTrigger(trigger1.getKey()), nullValue()); 44 | assertThat(jobStore.retrieveTrigger(trigger2.getKey()), not(nullValue())); 45 | 46 | verify(mockScheduleSignaler).signalSchedulingChange(0L); 47 | } 48 | 49 | @Test 50 | public void triggeredJobCompleteComplete() throws JobPersistenceException { 51 | jobStore.triggeredJobComplete(trigger1, job, Trigger.CompletedExecutionInstruction.SET_TRIGGER_COMPLETE); 52 | 53 | // ensure that neither trigger was deleted 54 | assertThat(jobStore.retrieveTrigger(trigger1.getKey()), not(nullValue())); 55 | assertThat(jobStore.retrieveTrigger(trigger2.getKey()), not(nullValue())); 56 | 57 | // ensure that the proper trigger was set to COMPLETE 58 | assertEquals(Trigger.TriggerState.COMPLETE, jobStore.getTriggerState(trigger1.getKey())); 59 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger2.getKey())); 60 | 61 | verify(mockScheduleSignaler).signalSchedulingChange(0L); 62 | } 63 | 64 | @Test 65 | public void triggeredJobCompleteError() throws JobPersistenceException { 66 | jobStore.triggeredJobComplete(trigger1, job, Trigger.CompletedExecutionInstruction.SET_TRIGGER_ERROR); 67 | 68 | // ensure that the proper trigger was set to ERROR 69 | assertEquals(Trigger.TriggerState.ERROR, jobStore.getTriggerState(trigger1.getKey())); 70 | assertEquals(Trigger.TriggerState.NORMAL, jobStore.getTriggerState(trigger2.getKey())); 71 | 72 | verify(mockScheduleSignaler).signalSchedulingChange(0L); 73 | } 74 | 75 | @Test 76 | public void triggeredJobCompleteAllError() throws JobPersistenceException { 77 | jobStore.triggeredJobComplete(trigger1, job, Trigger.CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR); 78 | 79 | // ensure that both triggers were set to ERROR 80 | assertEquals(Trigger.TriggerState.ERROR, jobStore.getTriggerState(trigger1.getKey())); 81 | assertEquals(Trigger.TriggerState.ERROR, jobStore.getTriggerState(trigger2.getKey())); 82 | 83 | verify(mockScheduleSignaler).signalSchedulingChange(0L); 84 | } 85 | 86 | @Test 87 | public void triggeredJobCompleteAllComplete() throws JobPersistenceException { 88 | jobStore.triggeredJobComplete(trigger1, job, Trigger.CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_COMPLETE); 89 | 90 | // ensure that both triggers were set to COMPLETE 91 | assertEquals(Trigger.TriggerState.COMPLETE, jobStore.getTriggerState(trigger1.getKey())); 92 | assertEquals(Trigger.TriggerState.COMPLETE, jobStore.getTriggerState(trigger2.getKey())); 93 | 94 | verify(mockScheduleSignaler).signalSchedulingChange(0L); 95 | } 96 | 97 | @Test 98 | public void triggeredJobCompletePersist() throws JobPersistenceException { 99 | JobDetail jobPersist = JobBuilder.newJob(TestJobPersist.class) 100 | .withIdentity("testJobPersist1", "jobGroupPersist1") 101 | .usingJobData("timeout", 42) 102 | .withDescription("I am describing a job!") 103 | .build(); 104 | CronTriggerImpl triggerPersist1 = getCronTrigger("triggerPersist1", "triggerPersistGroup1", jobPersist.getKey()); 105 | CronTriggerImpl triggerPersist2 = getCronTrigger("triggerPersist2", "triggerPersistGroup1", jobPersist.getKey()); 106 | storeJobAndTriggers(jobPersist, triggerPersist1, triggerPersist1); 107 | 108 | jobStore.triggeredJobComplete(triggerPersist1, jobPersist, Trigger.CompletedExecutionInstruction.SET_TRIGGER_COMPLETE); 109 | 110 | assertEquals(Trigger.TriggerState.COMPLETE, jobStore.getTriggerState(triggerPersist1.getKey())); 111 | } 112 | 113 | @Test 114 | public void triggeredJobCompleteNonConcurrent() throws JobPersistenceException { 115 | JobDetail job = JobBuilder.newJob(TestJobNonConcurrent.class) 116 | .withIdentity("testJobNonConcurrent1", "jobGroupNonConcurrent1") 117 | .usingJobData("timeout", 42) 118 | .withDescription("I am describing a job!") 119 | .build(); 120 | CronTriggerImpl trigger1 = getCronTrigger("triggerNonConcurrent1", "triggerNonConcurrentGroup1", job.getKey()); 121 | CronTriggerImpl trigger2 = getCronTrigger("triggerNonConcurrent2", "triggerNonConcurrentGroup1", job.getKey()); 122 | storeJobAndTriggers(job, trigger1, trigger2); 123 | 124 | jobStore.triggeredJobComplete(trigger1, job, Trigger.CompletedExecutionInstruction.SET_TRIGGER_COMPLETE); 125 | 126 | assertEquals(Trigger.TriggerState.COMPLETE, jobStore.getTriggerState(trigger1.getKey())); 127 | 128 | final String jobHashKey = schema.jobHashKey(job.getKey()); 129 | assertFalse(jedis.sismember(schema.blockedJobsSet(), jobHashKey)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/mixin/CronTriggerMixinTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.mixin; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import net.joelinn.quartz.jobstore.mixin.CronTriggerMixin; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.quartz.CronScheduleBuilder; 9 | import org.quartz.CronTrigger; 10 | import org.quartz.TriggerBuilder; 11 | import org.quartz.impl.triggers.CronTriggerImpl; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.collection.IsMapContaining.hasKey; 18 | import static org.junit.Assert.assertEquals; 19 | 20 | /** 21 | * Joe Linn 22 | * 7/15/2014 23 | */ 24 | public class CronTriggerMixinTest { 25 | protected ObjectMapper mapper; 26 | 27 | @Before 28 | public void setUp(){ 29 | mapper = new ObjectMapper(); 30 | mapper.addMixIn(CronTrigger.class, CronTriggerMixin.class); 31 | } 32 | 33 | @Test 34 | public void serialization(){ 35 | String cron = "0/5 * * * * ?"; 36 | CronTrigger trigger = TriggerBuilder.newTrigger() 37 | .forJob("testJob", "testGroup") 38 | .withIdentity("testTrigger", "testTriggerGroup") 39 | .withSchedule(CronScheduleBuilder.cronSchedule(cron)) 40 | .usingJobData("timeout", 5) 41 | .withDescription("A description!") 42 | .build(); 43 | 44 | Map triggerMap = mapper.convertValue(trigger, new TypeReference>() {}); 45 | 46 | assertThat(triggerMap, hasKey("name")); 47 | assertEquals("testTrigger", triggerMap.get("name")); 48 | assertThat(triggerMap, hasKey("group")); 49 | assertEquals("testTriggerGroup", triggerMap.get("group")); 50 | assertThat(triggerMap, hasKey("jobName")); 51 | assertEquals("testJob", triggerMap.get("jobName")); 52 | 53 | CronTriggerImpl cronTrigger = mapper.convertValue(triggerMap, CronTriggerImpl.class); 54 | 55 | assertEquals(trigger.getKey().getName(), cronTrigger.getKey().getName()); 56 | assertEquals(trigger.getKey().getGroup(), cronTrigger.getKey().getGroup()); 57 | assertEquals(trigger.getStartTime(), cronTrigger.getStartTime()); 58 | assertEquals(trigger.getCronExpression(), cronTrigger.getCronExpression()); 59 | assertEquals(trigger.getTimeZone(), cronTrigger.getTimeZone()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/mixin/JobDetailMixinTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.mixin; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import net.joelinn.quartz.TestJob; 6 | import net.joelinn.quartz.jobstore.mixin.JobDetailMixin; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.quartz.JobBuilder; 10 | import org.quartz.JobDetail; 11 | import org.quartz.impl.JobDetailImpl; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.collection.IsMapContaining.hasKey; 18 | import static org.junit.Assert.assertEquals; 19 | 20 | /** 21 | * Joe Linn 22 | * 7/15/2014 23 | */ 24 | public class JobDetailMixinTest { 25 | protected ObjectMapper mapper; 26 | 27 | @Before 28 | public void setUp(){ 29 | mapper = new ObjectMapper(); 30 | mapper.addMixIn(JobDetail.class, JobDetailMixin.class); 31 | } 32 | 33 | @Test 34 | public void serializeJobDetail() throws Exception { 35 | JobDetail testJob = JobBuilder.newJob(TestJob.class) 36 | .withIdentity("testJob", "testGroup") 37 | .usingJobData("timeout", 42) 38 | .withDescription("I am describing a job!") 39 | .build(); 40 | 41 | String json = mapper.writeValueAsString(testJob); 42 | Map jsonMap = mapper.readValue(json, new TypeReference>() {}); 43 | 44 | assertThat(jsonMap, hasKey("name")); 45 | assertEquals(testJob.getKey().getName(), jsonMap.get("name")); 46 | assertThat(jsonMap, hasKey("group")); 47 | assertEquals(testJob.getKey().getGroup(), jsonMap.get("group")); 48 | assertThat(jsonMap, hasKey("jobClass")); 49 | assertEquals(testJob.getJobClass().getName(), jsonMap.get("jobClass")); 50 | 51 | JobDetailImpl jobDetail = mapper.readValue(json, JobDetailImpl.class); 52 | 53 | assertEquals(testJob.getKey().getName(), jobDetail.getKey().getName()); 54 | assertEquals(testJob.getKey().getGroup(), jobDetail.getKey().getGroup()); 55 | assertEquals(testJob.getJobClass(), jobDetail.getJobClass()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/net/joelinn/quartz/mixin/SimpleTriggerMixinTest.java: -------------------------------------------------------------------------------- 1 | package net.joelinn.quartz.mixin; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import net.joelinn.quartz.jobstore.mixin.TriggerMixin; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.quartz.SimpleScheduleBuilder; 9 | import org.quartz.SimpleTrigger; 10 | import org.quartz.TriggerBuilder; 11 | import org.quartz.impl.triggers.SimpleTriggerImpl; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.collection.IsMapContaining.hasKey; 18 | import static org.junit.Assert.assertEquals; 19 | 20 | /** 21 | * Joe Linn 22 | * 7/15/2014 23 | */ 24 | public class SimpleTriggerMixinTest { 25 | protected ObjectMapper mapper; 26 | 27 | @Before 28 | public void setUp(){ 29 | mapper = new ObjectMapper(); 30 | mapper.addMixIn(SimpleTrigger.class, TriggerMixin.class); 31 | } 32 | 33 | @Test 34 | public void serialization(){ 35 | SimpleTrigger trigger = TriggerBuilder.newTrigger() 36 | .forJob("testJob", "testGroup") 37 | .withIdentity("testTrigger", "testTriggerGroup") 38 | .usingJobData("timeout", 5) 39 | .withDescription("A description!") 40 | .withSchedule(SimpleScheduleBuilder.repeatHourlyForever()) 41 | .build(); 42 | 43 | Map triggerMap = mapper.convertValue(trigger, new TypeReference>() {}); 44 | 45 | assertThat(triggerMap, hasKey("name")); 46 | assertEquals("testTrigger", triggerMap.get("name")); 47 | assertThat(triggerMap, hasKey("group")); 48 | assertEquals("testTriggerGroup", triggerMap.get("group")); 49 | assertThat(triggerMap, hasKey("jobName")); 50 | assertEquals("testJob", triggerMap.get("jobName")); 51 | 52 | SimpleTriggerImpl simpleTrigger = mapper.convertValue(triggerMap, SimpleTriggerImpl.class); 53 | 54 | assertEquals(trigger.getKey().getName(), simpleTrigger.getKey().getName()); 55 | assertEquals(trigger.getKey().getGroup(), simpleTrigger.getKey().getGroup()); 56 | assertEquals(trigger.getStartTime(), simpleTrigger.getStartTime()); 57 | assertEquals(trigger.getRepeatInterval(), simpleTrigger.getRepeatInterval()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %C{5}:%L - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------