├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── github
│ └── davidmarquis
│ └── redisscheduler
│ ├── PollingThread.java
│ ├── RedisConnectException.java
│ ├── RedisDriver.java
│ ├── RedisTaskScheduler.java
│ ├── SchedulerIdentity.java
│ ├── TaskRunner.java
│ ├── TaskScheduler.java
│ ├── TaskTriggerListener.java
│ └── drivers
│ ├── jedis
│ └── JedisDriver.java
│ ├── lettuce
│ └── LettuceDriver.java
│ └── spring
│ └── RedisTemplateDriver.java
└── test
├── java
└── com
│ └── github
│ └── davidmarquis
│ └── redisscheduler
│ ├── AcceptanceTestSuite.java
│ ├── JedisIntegrationTest.java
│ ├── LettuceIntegrationTest.java
│ ├── RedisTaskSchedulerTest.java
│ ├── SpringIntegrationTest.java
│ └── lib
│ ├── LatchedTriggerListener.java
│ ├── StartRedis.java
│ └── StubbedClock.java
└── resources
├── application-context-test.xml
├── logback.xml
└── test-config.properties
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.ipr
3 | .idea
4 | target
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2011-2013 David Marquis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | redis-scheduler
2 | ===============
3 |
4 | Distributed Scheduler using Redis for Java applications.
5 |
6 | What is this?
7 | -------------
8 |
9 | `redis-scheduler` is a Java implementation of a distributed scheduler using Redis. It has the following features:
10 |
11 | - **Useable in a distributed environment**: Uses Redis transactions for effectively preventing a task to be run on
12 | multiple instances of the same application.
13 | - **Lightweight**: Uses a single thread.
14 | - **Configurable polling**: Polling delay can be configured to tweak execution precision (at the cost of performance)
15 | - **Multiple schedulers support**: You can create multiple schedulers in the same logical application if you need to.
16 | - **Support for multiple client libraries**: Drivers exist for [Jedis](https://github.com/xetorthio/jedis), [Lettuce](https://lettuce.io/) and [Spring Data's RedisTemplate](https://projects.spring.io/spring-data-redis/)
17 |
18 | High level concepts
19 | -------------------
20 |
21 | #### Scheduled Task
22 |
23 | A scheduled task is a job that you need to execute in the future at a particular time.
24 | In `redis-scheduler`, a task is represented solely by an arbitrary string identifier that has no particular meaning to the library.
25 | It's your application that has to make sense of this identifier.
26 |
27 | #### Scheduler
28 |
29 | `RedisTaskScheduler`: This interface is where you submit your tasks for future execution. Once submitted, a task will only be
30 | executed at or after the trigger time you provide.
31 |
32 | #### `TaskTriggerListener` interface
33 |
34 | This is the main interface you must implement to actually run the tasks once they are due for execution. The library will
35 | call the `taskTriggered` method for each task that is due for execution.
36 |
37 |
38 | Building the project
39 | --------------------
40 |
41 | ``` bash
42 | mvn package
43 | ```
44 |
45 | Maven dependency
46 | ----------------
47 |
48 | This artifact is published on Maven Central:
49 |
50 | ``` xml
51 |
52 | com.github.davidmarquis
53 | redis-scheduler
54 | 3.0.0
55 |
56 | ```
57 |
58 | You'll need to add one of the specific dependencies to use the different available drivers:
59 |
60 | To use with Lettuce:
61 |
62 | ``` xml
63 |
64 | io.lettuce
65 | lettuce-core
66 | 5.0.3.RELEASE
67 |
68 | ```
69 |
70 | To use with Jedis:
71 |
72 | ``` xml
73 |
74 | redis.clients
75 | jedis
76 | 2.9.0
77 |
78 | ```
79 |
80 | To use with Spring Data Redis:
81 |
82 | ``` xml
83 |
84 | org.springframework.data
85 | spring-data-redis
86 | 1.8.11.RELEASE
87 |
88 | ```
89 |
90 |
91 | Usage with Lettuce
92 | ------------------
93 |
94 | The scheduler must be instantiated with the `LettuceDriver`:
95 |
96 | ``` java
97 | RedisClient client = RedisClient.create(RedisURI.create("localhost", 6379));
98 | RedisTaskScheduler scheduler = new RedisTaskScheduler(new LettuceDriver(client), new YourTaskTriggerListener());
99 |
100 | scheduler.start();
101 | ```
102 |
103 | Usage with Jedis
104 | ----------------
105 |
106 | The scheduler must be instantiated with the `JedisDriver`:
107 |
108 | ``` java
109 | JedisPool pool = new JedisPool("localhost", 6379);
110 | RedisTaskScheduler scheduler = new RedisTaskScheduler(new JedisDriver(pool), new YourTaskTriggerListener());
111 |
112 | scheduler.start();
113 | ```
114 |
115 | Usage with Spring
116 | -----------------
117 |
118 | First declare the base beans for Redis connectivity (if not already done in your project). This part can be different
119 | for your project.
120 |
121 | ``` xml
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | ```
138 |
139 | Finally, declare the scheduler instance:
140 |
141 | ``` xml
142 |
143 |
144 |
145 |
146 |
147 |
148 | ```
149 |
150 | As noted above, `RedisTaskScheduler` expects an implementation of the `TaskTriggerListener` interface which it will notify when a task is due for execution. You must implement this interface yourself and provide it to the scheduler as a constructor argument.
151 |
152 | See the the test Spring context in `test/resources/application-context-test.xml` for a complete working example of the setup.
153 |
154 |
155 | Scheduling a task in the future
156 | -------------------------------
157 |
158 | ``` java
159 | scheduler.schedule("mytask", new GregorianCalendar(2015, Calendar.JANUARY, 1, 4, 45, 0));
160 | ```
161 |
162 | This would schedule a task with ID "mytask" to be run at 4:45AM on January 1st 2015.
163 |
164 | Be notified once a task is due for execution
165 | --------------------------------------------
166 |
167 | ``` java
168 | public class MyTaskTriggerListener implements TaskTriggerListener {
169 | public void taskTriggered(String taskId) {
170 | System.out.printf("Task %s is due for execution.", taskId);
171 | }
172 | }
173 | ```
174 |
175 | Customizing polling delay
176 | ----------------------------------
177 |
178 | By default, polling delay is set to a few seconds (see implementation `RedisTaskScheduler` for actual value). If
179 | you need your tasks to be triggered with more precision, decrease the polling delay using the `pollingDelayMillis` attribute of `RedisTaskScheduler`:
180 |
181 | In Java:
182 |
183 | ``` java
184 | scheduler.setPollingDelayMillis(500);
185 | ```
186 |
187 | With Spring:
188 |
189 | ``` xml
190 |
191 |
192 |
193 | ```
194 |
195 | Increasing polling delay comes with a cost: higher load on Redis and your connection.
196 | Try to find the best balance for your needs.
197 |
198 | Retry polling when a Redis connection error happens
199 | ---------------------------------------------------
200 |
201 | Retries can be configured using the `maxRetriesOnConnectionFailure` property on `RedisTaskScheduler`:
202 |
203 | In Java:
204 |
205 | ``` java
206 | scheduler.setMaxRetriesOnConnectionFailure(5);
207 | ```
208 |
209 | With Spring:
210 |
211 | ``` xml
212 |
213 |
214 |
215 | ```
216 |
217 | After the specified number of retries, the polling thread will stop and log an error.
218 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.github.davidmarquis
6 | redis-scheduler
7 | 3.0.1-SNAPSHOT
8 | jar
9 |
10 | redis-scheduler
11 | Java implementation of a lightweight distributed task scheduler backed by Redis
12 | https://github.com/davidmarquis/redis-scheduler
13 |
14 |
15 |
16 | MIT
17 | http://www.opensource.org/licenses/mit-license.php
18 | repo
19 |
20 |
21 |
22 |
23 | scm:git:git@github.com:davidmarquis/redis-scheduler.git
24 | scm:git:git@github.com:davidmarquis/redis-scheduler.git
25 | git@github.com:davidmarquis/redis-scheduler.git
26 | HEAD
27 |
28 |
29 |
30 |
31 | davidmarquis
32 | David Marquis
33 | david@radiant3.ca
34 |
35 |
36 |
37 |
38 |
39 | ossrh
40 | https://oss.sonatype.org/content/repositories/snapshots
41 |
42 |
43 | ossrh
44 | https://oss.sonatype.org/service/local/staging/deploy/maven2/
45 |
46 |
47 |
48 |
49 | UTF-8
50 | 1.8
51 |
52 |
53 |
54 |
55 | javax.annotation
56 | javax.annotation-api
57 | 1.3.1
58 |
59 |
60 |
61 |
62 | io.lettuce
63 | lettuce-core
64 | 5.0.3.RELEASE
65 | provided
66 |
67 |
68 |
69 |
70 | redis.clients
71 | jedis
72 | 2.9.0
73 | provided
74 |
75 |
76 |
77 |
78 | org.springframework.data
79 | spring-data-redis
80 | 1.8.11.RELEASE
81 | provided
82 |
83 |
84 | org.apache.commons
85 | commons-pool2
86 | 2.2
87 | provided
88 |
89 |
90 | org.springframework
91 | spring-core
92 | 4.3.15.RELEASE
93 | provided
94 |
95 |
96 | org.springframework
97 | spring-test
98 | 4.3.15.RELEASE
99 | test
100 |
101 |
102 |
103 |
104 | ch.qos.logback
105 | logback-classic
106 | 1.2.3
107 | provided
108 |
109 |
110 | com.github.kstyrc
111 | embedded-redis
112 | 0.6
113 | test
114 |
115 |
116 | junit
117 | junit
118 | 4.12
119 | test
120 |
121 |
122 | org.hamcrest
123 | hamcrest-all
124 | 1.3
125 | test
126 |
127 |
128 | org.mockito
129 | mockito-all
130 | 1.9.5
131 | test
132 |
133 |
134 |
135 |
136 |
137 | release
138 |
139 |
140 | performRelease
141 | true
142 |
143 |
144 |
145 |
146 |
147 | org.apache.maven.plugins
148 | maven-source-plugin
149 | 2.2.1
150 |
151 |
152 | attach-sources
153 |
154 | jar-no-fork
155 |
156 |
157 |
158 |
159 |
160 | org.apache.maven.plugins
161 | maven-javadoc-plugin
162 | 3.0.0
163 |
164 |
165 | attach-javadocs
166 |
167 | jar
168 |
169 |
170 |
171 |
172 |
173 | org.apache.maven.plugins
174 | maven-gpg-plugin
175 | 1.5
176 |
177 |
178 | sign-artifacts
179 | verify
180 |
181 | sign
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 | disable-java8-doclint
191 |
192 | [1.8,)
193 |
194 |
195 | -Xdoclint:none
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | org.apache.maven.plugins
204 | maven-compiler-plugin
205 | 3.7.0
206 |
207 | ${java.version}
208 | ${java.version}
209 |
210 |
211 |
212 | org.apache.maven.plugins
213 | maven-release-plugin
214 | 2.5.3
215 |
216 | true
217 | false
218 | release
219 | deploy
220 |
221 |
222 |
223 | org.sonatype.plugins
224 | nexus-staging-maven-plugin
225 | 1.6.7
226 | true
227 |
228 | ossrh
229 | https://oss.sonatype.org/
230 | true
231 |
232 |
233 |
234 |
235 |
236 | ${project.basedir}/src/main/resources
237 |
238 |
239 |
240 |
241 | src/test/resources
242 | true
243 |
244 |
245 |
246 |
247 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/PollingThread.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | class PollingThread extends Thread {
7 |
8 | private static final Logger log = LoggerFactory.getLogger(TaskScheduler.class);
9 |
10 | private TaskRunner runner;
11 | private int maxRetriesOnConnectionFailure;
12 | private int pollingDelayMillis;
13 |
14 | private boolean stopRequested = false;
15 | private int numRetriesAttempted = 0;
16 |
17 | PollingThread(TaskRunner runner, int maxRetriesOnConnectionFailure, int pollingDelayMillis) {
18 | this.runner = runner;
19 | this.maxRetriesOnConnectionFailure = maxRetriesOnConnectionFailure;
20 | this.pollingDelayMillis = pollingDelayMillis;
21 | }
22 |
23 | void requestStop() {
24 | stopRequested = true;
25 | }
26 |
27 | @Override
28 | public void run() {
29 | try {
30 | while (!stopRequested && !isMaxRetriesAttemptsReached()) {
31 |
32 | try {
33 | attemptTriggerNextTask();
34 | } catch (InterruptedException e) {
35 | break;
36 | }
37 | }
38 | } catch (Exception e) {
39 | log.error(String.format(
40 | "[%s] Error while polling scheduled tasks. " +
41 | "No additional scheduled task will be triggered until the application is restarted.", getName()), e);
42 | }
43 |
44 | if (isMaxRetriesAttemptsReached()) {
45 | log.error(String.format("[%s] Maximum number of retries (%s) after Redis connection failure has been reached. " +
46 | "No additional scheduled task will be triggered until the application is restarted.", getName(), maxRetriesOnConnectionFailure));
47 | } else {
48 | log.info(String.format("[%s] Redis Scheduler stopped", getName()));
49 | }
50 | }
51 |
52 | private void attemptTriggerNextTask() throws InterruptedException {
53 | try {
54 | boolean taskTriggered = runner.triggerNextTaskIfFound();
55 |
56 | // if a task was triggered, we'll try again immediately. This will help to speed up the execution
57 | // process if a few tasks were due for execution.
58 | if (!taskTriggered) {
59 | sleep(pollingDelayMillis);
60 | }
61 |
62 | resetRetriesAttemptsCount();
63 | } catch (RedisConnectException e) {
64 | incrementRetriesAttemptsCount();
65 | log.warn(String.format("Connection failure during scheduler polling (attempt %s/%s)", numRetriesAttempted, maxRetriesOnConnectionFailure));
66 | }
67 | }
68 |
69 | private boolean isMaxRetriesAttemptsReached() {
70 | return numRetriesAttempted >= maxRetriesOnConnectionFailure;
71 | }
72 |
73 | private void resetRetriesAttemptsCount() {
74 | numRetriesAttempted = 0;
75 | }
76 |
77 | private void incrementRetriesAttemptsCount() {
78 | numRetriesAttempted++;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/RedisConnectException.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 |
4 | public class RedisConnectException extends RuntimeException {
5 | public RedisConnectException(Throwable cause) {
6 | super(cause);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/RedisDriver.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | import java.util.Optional;
4 | import java.util.function.Consumer;
5 | import java.util.function.Function;
6 |
7 | public interface RedisDriver {
8 |
9 | T fetch(Function block);
10 |
11 | default void execute(Consumer block) {
12 | fetch((Function) commands -> {
13 | block.accept(commands);
14 | return null;
15 | });
16 | }
17 |
18 | interface Commands {
19 | void addToSetWithScore(String key, String taskId, long score);
20 |
21 | void removeFromSet(String key, String taskId);
22 |
23 | void remove(String key);
24 |
25 | void watch(String key);
26 |
27 | void unwatch();
28 |
29 | void multi();
30 |
31 | boolean exec();
32 |
33 | Optional firstByScore(String key, long minScore, long maxScore);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/RedisTaskScheduler.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | import javax.annotation.PostConstruct;
7 | import javax.annotation.PreDestroy;
8 | import java.time.Clock;
9 | import java.time.Instant;
10 | import java.util.Optional;
11 |
12 | public class RedisTaskScheduler implements TaskScheduler, TaskRunner {
13 |
14 | private static final Logger log = LoggerFactory.getLogger(TaskScheduler.class);
15 |
16 | private static final String DEFAULT_SCHEDULER_NAME = "scheduler";
17 |
18 | private Clock clock = Clock.systemDefaultZone();
19 | private RedisDriver driver;
20 | private TaskTriggerListener listener;
21 |
22 | private SchedulerIdentity identity = SchedulerIdentity.of(DEFAULT_SCHEDULER_NAME);
23 |
24 | private PollingThread pollingThread;
25 | private int pollingDelayMillis = 10000;
26 | private int maxRetriesOnConnectionFailure = 1;
27 |
28 | public RedisTaskScheduler(RedisDriver driver, TaskTriggerListener listener) {
29 | this.driver = driver;
30 | this.listener = listener;
31 | }
32 |
33 | public void runNow(String taskId) {
34 | scheduleAt(taskId, clock.instant());
35 | }
36 |
37 | public void scheduleAt(String taskId, Instant triggerTime) {
38 | if (triggerTime == null) {
39 | throw new IllegalArgumentException("A trigger time must be provided.");
40 | }
41 |
42 | driver.execute(commands -> commands.addToSetWithScore(identity.key(), taskId, triggerTime.toEpochMilli()));
43 | }
44 |
45 | @Override
46 | public void unschedule(String taskId) {
47 | driver.execute(commands -> commands.removeFromSet(identity.key(), taskId));
48 | }
49 |
50 | @Override
51 | public void unscheduleAllTasks() {
52 | driver.execute(commands -> commands.remove(identity.key()));
53 | }
54 |
55 | @PostConstruct
56 | public void start() {
57 | pollingThread = new PollingThread(this, maxRetriesOnConnectionFailure, pollingDelayMillis);
58 | pollingThread.setName(identity.name() + "-polling");
59 |
60 | pollingThread.start();
61 |
62 | log.info(String.format("[%s] Started Redis Scheduler (polling freq: [%sms])", identity.name(), pollingDelayMillis));
63 | }
64 |
65 | public void stop() {
66 | close();
67 | }
68 |
69 | @Override
70 | @PreDestroy
71 | public void close() {
72 | if (pollingThread != null) {
73 | pollingThread.requestStop();
74 | }
75 | }
76 |
77 | public void setClock(Clock clock) {
78 | this.clock = clock;
79 | }
80 |
81 | /**
82 | * If multiple schedulers are needed for the same application, customize their names to differentiate them in logs.
83 | */
84 | public void setSchedulerName(String schedulerName) {
85 | this.identity = SchedulerIdentity.of(schedulerName);
86 | }
87 |
88 | /**
89 | * Delay between polling of the scheduled tasks. The lower the value, the best precision in triggering tasks.
90 | * However, the lower the value, the higher the load on Redis.
91 | */
92 | public void setPollingDelayMillis(int pollingDelayMillis) {
93 | this.pollingDelayMillis = pollingDelayMillis;
94 | }
95 |
96 | public void setMaxRetriesOnConnectionFailure(int maxRetriesOnConnectionFailure) {
97 | this.maxRetriesOnConnectionFailure = maxRetriesOnConnectionFailure;
98 | }
99 |
100 | @SuppressWarnings("unchecked")
101 | public boolean triggerNextTaskIfFound() {
102 | return driver.fetch(commands -> {
103 | boolean taskWasTriggered = false;
104 |
105 | commands.watch(identity.key());
106 |
107 | Optional nextTask = commands.firstByScore(identity.key(), 0, clock.millis());
108 |
109 | if (nextTask.isPresent()) {
110 | String nextTaskId = nextTask.get();
111 |
112 | commands.multi();
113 | commands.removeFromSet(identity.key(), nextTaskId);
114 | boolean executionSuccess = commands.exec();
115 |
116 | if (executionSuccess) {
117 | log.debug(String.format("[%s] Triggering execution of task [%s]", identity.name(), nextTaskId));
118 |
119 | tryTaskExecution(nextTaskId);
120 | taskWasTriggered = true;
121 | } else {
122 | log.warn(String.format("[%s] Race condition detected for triggering of task [%s]. " +
123 | "The task has probably been triggered by another instance of this application.", identity.name(), nextTaskId));
124 | }
125 | } else {
126 | commands.unwatch();
127 | }
128 |
129 | return taskWasTriggered;
130 | });
131 | }
132 |
133 | private void tryTaskExecution(String task) {
134 | try {
135 | listener.taskTriggered(task);
136 | } catch (Exception e) {
137 | log.error(String.format("[%s] Error during execution of task [%s]", identity.name(), task), e);
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/SchedulerIdentity.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | class SchedulerIdentity {
4 | private static final String REDIS_KEY_FORMAT = "redis-scheduler.%s";
5 |
6 | private String name;
7 |
8 | private SchedulerIdentity(String name) {
9 | this.name = name;
10 | }
11 |
12 | String key() {
13 | return String.format(REDIS_KEY_FORMAT, name);
14 | }
15 |
16 | String name() {
17 | return name;
18 | }
19 |
20 | static SchedulerIdentity of(String name) {
21 | return new SchedulerIdentity(name);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/TaskRunner.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | public interface TaskRunner {
4 | boolean triggerNextTaskIfFound();
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/TaskScheduler.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | import java.io.Closeable;
4 | import java.time.Instant;
5 |
6 | /**
7 | * Schedules arbitrary tasks in the future.
8 | */
9 | public interface TaskScheduler extends Closeable {
10 | /**
11 | * Runs a task immediately.
12 | *
13 | * @param taskId an arbitrary task identifier. That same identifier will be used in TaskTriggerListener callback
14 | * once the task is due for execution.
15 | */
16 | void runNow(String taskId);
17 |
18 | /**
19 | * Schedules a task for future execution.
20 | *
21 | * @param taskId an arbitrary task identifier. That same identifier will be used in TaskTriggerListener callback
22 | * once the task is due for execution.
23 | * @param trigger the time at which we want the task to be executed. If this value is null
or in the past,
24 | * then the task will be immediately scheduled for execution.
25 | */
26 | void scheduleAt(String taskId, Instant trigger);
27 |
28 | /**
29 | * Removes all currently scheduled tasks from the scheduler.
30 | */
31 | void unscheduleAllTasks();
32 |
33 | /**
34 | * Removes a specific task from the scheduler. If the task was not previously scheduled, then calling this method
35 | * has no particular effect.
36 | *
37 | * @param taskId The task ID to remove.
38 | */
39 | void unschedule(String taskId);
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/TaskTriggerListener.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | /**
4 | * Callback interface that will get executed when a previously-scheduled task is due for execution.
5 | */
6 | public interface TaskTriggerListener {
7 |
8 | /**
9 | * Called by the scheduler once a task is due for execution.
10 | *
11 | * @param taskId the task ID that was originally submitted to the RedisTaskScheduler.
12 | */
13 | void taskTriggered(String taskId);
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/drivers/jedis/JedisDriver.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler.drivers.jedis;
2 |
3 | import com.github.davidmarquis.redisscheduler.RedisConnectException;
4 | import com.github.davidmarquis.redisscheduler.RedisDriver;
5 | import redis.clients.jedis.Jedis;
6 | import redis.clients.jedis.JedisPool;
7 | import redis.clients.jedis.Transaction;
8 |
9 | import java.io.IOException;
10 | import java.util.Optional;
11 | import java.util.function.Function;
12 |
13 | public class JedisDriver implements RedisDriver {
14 |
15 | private JedisPool jedisPool;
16 |
17 | public JedisDriver(JedisPool jedisPool) {
18 | this.jedisPool = jedisPool;
19 | }
20 |
21 | @Override
22 | public T fetch(Function block) {
23 | try (Jedis jedis = jedisPool.getResource()) {
24 | return block.apply(new JedisCommands(jedis));
25 | }
26 | }
27 |
28 | private static class JedisCommands implements Commands {
29 |
30 | private Jedis jedis;
31 |
32 | private Transaction txn;
33 |
34 | private JedisCommands(Jedis jedis) {
35 | this.jedis = jedis;
36 | }
37 |
38 | @Override
39 | public void addToSetWithScore(String key, String taskId, long score) {
40 | jedis.zadd(key, score, taskId);
41 | }
42 |
43 | @Override
44 | public void removeFromSet(String key, String taskId) {
45 | if (txn != null) {
46 | txn.zrem(key, taskId);
47 | } else {
48 | jedis.zrem(key, taskId);
49 | }
50 | }
51 |
52 | @Override
53 | public void remove(String key) {
54 | jedis.del(key);
55 | }
56 |
57 | @Override
58 | public void watch(String key) {
59 | jedis.watch(key);
60 | }
61 |
62 | @Override
63 | public void unwatch() {
64 | jedis.unwatch();
65 | }
66 |
67 | @Override
68 | public void multi() {
69 | txn = jedis.multi();
70 | }
71 |
72 | @Override
73 | public boolean exec() {
74 | try {
75 | return Optional.ofNullable(txn)
76 | .map(Transaction::exec)
77 | .map(col -> !col.isEmpty())
78 | .orElse(false);
79 | } finally {
80 | try {
81 | txn.close();
82 | } catch (IOException e) {
83 | throw new RedisConnectException(e);
84 | }
85 | txn = null;
86 | }
87 | }
88 |
89 | @Override
90 | public Optional firstByScore(String key, long minScore, long maxScore) {
91 | return jedis.zrangeByScore(key, minScore, maxScore, 0, 1)
92 | .stream()
93 | .findFirst();
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/drivers/lettuce/LettuceDriver.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler.drivers.lettuce;
2 |
3 | import com.github.davidmarquis.redisscheduler.RedisConnectException;
4 | import com.github.davidmarquis.redisscheduler.RedisDriver;
5 | import io.lettuce.core.Limit;
6 | import io.lettuce.core.Range;
7 | import io.lettuce.core.RedisClient;
8 | import io.lettuce.core.RedisConnectionException;
9 | import io.lettuce.core.api.StatefulRedisConnection;
10 | import io.lettuce.core.api.sync.RedisCommands;
11 |
12 | import java.util.Optional;
13 | import java.util.function.Function;
14 |
15 | /**
16 | * Driver using Lettuce in synchronous mode (see lettuce.io)
17 | */
18 | public class LettuceDriver implements RedisDriver {
19 | private final RedisClient client;
20 |
21 | public LettuceDriver(RedisClient client) {
22 | this.client = client;
23 | }
24 |
25 | @Override
26 | public T fetch(Function block) {
27 | try (StatefulRedisConnection connection = client.connect()) {
28 | RedisCommands commands = connection.sync();
29 | return block.apply(new LettuceCommands(commands));
30 | } catch (RedisConnectionException e) {
31 | throw new RedisConnectException(e);
32 | }
33 | }
34 |
35 | private static class LettuceCommands implements Commands {
36 | private final RedisCommands commands;
37 |
38 | private LettuceCommands(RedisCommands commands) {
39 | this.commands = commands;
40 | }
41 |
42 | @Override
43 | public void addToSetWithScore(String key, String taskId, long score) {
44 | commands.zadd(key, score, taskId);
45 | }
46 |
47 | @Override
48 | public void removeFromSet(String key, String taskId) {
49 | commands.zrem(key, taskId);
50 | }
51 |
52 | @Override
53 | public void remove(String key) {
54 | commands.del(key);
55 | }
56 |
57 | @Override
58 | public void watch(String key) {
59 | commands.watch(key);
60 | }
61 |
62 | @Override
63 | public void unwatch() {
64 | commands.unwatch();
65 | }
66 |
67 | @Override
68 | public void multi() {
69 | commands.multi();
70 | }
71 |
72 | @Override
73 | public boolean exec() {
74 | return !commands.exec().isEmpty();
75 | }
76 |
77 | @Override
78 | public Optional firstByScore(String key, long minScore, long maxScore) {
79 | return commands.zrangebyscore(key, Range.create(minScore, maxScore), Limit.create(0, 1))
80 | .stream()
81 | .findFirst();
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/java/com/github/davidmarquis/redisscheduler/drivers/spring/RedisTemplateDriver.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler.drivers.spring;
2 |
3 | import com.github.davidmarquis.redisscheduler.RedisConnectException;
4 | import com.github.davidmarquis.redisscheduler.RedisDriver;
5 | import org.springframework.dao.DataAccessException;
6 | import org.springframework.data.redis.RedisConnectionFailureException;
7 | import org.springframework.data.redis.core.RedisOperations;
8 | import org.springframework.data.redis.core.RedisTemplate;
9 | import org.springframework.data.redis.core.SessionCallback;
10 |
11 | import java.util.Optional;
12 | import java.util.function.Function;
13 |
14 | /**
15 | * Driver using Spring Data Redis
16 | */
17 | public class RedisTemplateDriver implements RedisDriver {
18 |
19 | private RedisTemplate redisTemplate;
20 |
21 | public RedisTemplateDriver(RedisTemplate redisTemplate) {
22 | this.redisTemplate = redisTemplate;
23 | }
24 |
25 | @Override
26 | public T fetch(Function block) {
27 | try {
28 | return redisTemplate.execute(new SessionCallback() {
29 | @Override
30 | @SuppressWarnings("unchecked")
31 | public T execute(RedisOperations operations) throws DataAccessException {
32 | RedisConnectionCommands commands = new RedisConnectionCommands((RedisOperations) operations);
33 | return block.apply(commands);
34 | }
35 | });
36 | } catch (RedisConnectionFailureException e) {
37 | throw new RedisConnectException(e);
38 | }
39 | }
40 |
41 | private static class RedisConnectionCommands implements Commands {
42 | private RedisOperations ops;
43 |
44 | private RedisConnectionCommands(RedisOperations ops) {
45 | this.ops = ops;
46 | }
47 |
48 | @Override
49 | public void addToSetWithScore(String key, String taskId, long score) {
50 | ops.opsForZSet().add(key, taskId, score);
51 | }
52 |
53 | @Override
54 | public void removeFromSet(String key, String taskId) {
55 | ops.opsForZSet().remove(key, taskId);
56 | }
57 |
58 | @Override
59 | public void remove(String key) {
60 | ops.delete(key);
61 | }
62 |
63 | @Override
64 | public void watch(String key) {
65 | ops.watch(key);
66 | }
67 |
68 | @Override
69 | public void unwatch() {
70 | ops.unwatch();
71 | }
72 |
73 | @Override
74 | public void multi() {
75 | ops.multi();
76 | }
77 |
78 | @Override
79 | public boolean exec() {
80 | return ops.exec() != null;
81 | }
82 |
83 | @Override
84 | public Optional firstByScore(String key, long minScore, long maxScore) {
85 | return ops.opsForZSet()
86 | .rangeByScore(key, minScore, maxScore, 0, 1)
87 | .stream()
88 | .findFirst();
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/test/java/com/github/davidmarquis/redisscheduler/AcceptanceTestSuite.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | import com.github.davidmarquis.redisscheduler.lib.LatchedTriggerListener;
4 | import com.github.davidmarquis.redisscheduler.lib.StartRedis;
5 | import com.github.davidmarquis.redisscheduler.lib.StubbedClock;
6 | import org.junit.After;
7 | import org.junit.Before;
8 | import org.junit.ClassRule;
9 | import org.junit.Test;
10 |
11 | import java.util.List;
12 |
13 | import static java.util.Arrays.asList;
14 | import static java.util.concurrent.TimeUnit.HOURS;
15 | import static org.hamcrest.CoreMatchers.is;
16 | import static org.hamcrest.MatcherAssert.assertThat;
17 |
18 | public abstract class AcceptanceTestSuite {
19 |
20 | @ClassRule
21 | public static StartRedis redis = new StartRedis();
22 |
23 | // Actors:
24 | RedisTaskScheduler scheduler;
25 | StubbedClock clock = new StubbedClock();
26 | LatchedTriggerListener taskTriggerListener = new LatchedTriggerListener();
27 |
28 | protected abstract void provideActors();
29 |
30 | @Before
31 | public void setup() {
32 | provideActors();
33 |
34 | scheduler.unscheduleAllTasks();
35 | taskTriggerListener.reset();
36 |
37 | clock.is("20180405 10:00");
38 | }
39 |
40 | @After
41 | public void tearDown() {
42 | scheduler.unscheduleAllTasks();
43 | taskTriggerListener.reset();
44 | }
45 |
46 | @Test
47 | public void canTriggerTask() throws InterruptedException {
48 | scheduler.scheduleAt("mytask", clock.in(2, HOURS));
49 | clock.fastForward(2, HOURS);
50 |
51 | checkExactTasksTriggered("mytask");
52 | }
53 |
54 | @Test
55 | public void canTriggerMultipleTasks() throws InterruptedException {
56 | scheduler.scheduleAt("first", clock.in(1, HOURS));
57 | scheduler.scheduleAt("second", clock.in(2, HOURS));
58 | clock.fastForward(2, HOURS);
59 |
60 | checkExactTasksTriggered("first", "second");
61 | }
62 |
63 | @Test
64 | public void canTriggerPastTasks() throws InterruptedException {
65 | scheduler.scheduleAt("mytask1", clock.in(1, HOURS));
66 | scheduler.scheduleAt("mytask2", clock.in(2, HOURS));
67 | scheduler.scheduleAt("mytask3", clock.in(5, HOURS));
68 | clock.fastForward(3, HOURS);
69 |
70 | checkOnlyTasksTriggered("mytask1", "mytask2");
71 | }
72 |
73 | @Test
74 | public void cannotTriggerFutureTasks() throws InterruptedException {
75 | scheduler.scheduleAt("mytask", clock.in(1, HOURS));
76 |
77 | assertNoTaskTriggered();
78 | }
79 |
80 | @Test
81 | public void canScheduleInThePast() throws InterruptedException {
82 | scheduler.scheduleAt("mytask", clock.in(-1, HOURS));
83 |
84 | checkExactTasksTriggered("mytask");
85 | }
86 |
87 | @Test(expected = IllegalArgumentException.class)
88 | public void schedulingAtNullTimeRaisesException() {
89 | scheduler.scheduleAt("mytask", null);
90 | }
91 |
92 | @Test
93 | public void runNowImmediatelyTriggers() throws InterruptedException {
94 | scheduler.runNow("immediate");
95 |
96 | checkExactTasksTriggered("immediate");
97 | }
98 |
99 | @Test
100 | public void canRescheduleTask() throws InterruptedException {
101 | scheduler.scheduleAt("mytask", clock.in(5, HOURS));
102 | scheduler.scheduleAt("mytask", clock.in(1, HOURS));
103 | clock.fastForward(2, HOURS);
104 |
105 | checkExactTasksTriggered("mytask");
106 | }
107 |
108 | @Test
109 | public void canUnscheduleTask() throws InterruptedException {
110 | scheduler.scheduleAt("mytask1", clock.in(1, HOURS));
111 | scheduler.scheduleAt("mytask2", clock.in(1, HOURS));
112 | scheduler.unschedule("mytask2");
113 | clock.fastForward(2, HOURS);
114 |
115 | checkOnlyTasksTriggered("mytask1");
116 | }
117 |
118 | private void checkExactTasksTriggered(String... tasks) throws InterruptedException {
119 | taskTriggerListener.waitUntilTriggeredCount(tasks.length, 1000);
120 |
121 | assertTasksTriggered(tasks);
122 | }
123 |
124 | private void checkOnlyTasksTriggered(String... tasks) throws InterruptedException {
125 | // if only a subset of the scheduled tasks are expected to be triggered, then we need to wait for a while.
126 | Thread.sleep(1000);
127 |
128 | assertTasksTriggered(tasks);
129 | }
130 |
131 | private void assertTasksTriggered(String... tasks) {
132 | List triggeredTasks = taskTriggerListener.getTriggeredTasks();
133 | assertThat("Triggered tasks count", triggeredTasks.size(), is(tasks.length));
134 | assertThat("Triggered tasks", triggeredTasks, is(asList(tasks)));
135 | }
136 |
137 | private void assertNoTaskTriggered() throws InterruptedException {
138 | Thread.sleep(1000);
139 |
140 | assertThat("No tasks should triggered", taskTriggerListener.getTriggeredTasks().size(), is(0));
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/test/java/com/github/davidmarquis/redisscheduler/JedisIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | import com.github.davidmarquis.redisscheduler.drivers.jedis.JedisDriver;
4 | import org.junit.After;
5 | import org.junit.AfterClass;
6 | import redis.clients.jedis.JedisPool;
7 |
8 | public class JedisIntegrationTest extends AcceptanceTestSuite {
9 |
10 | private static final JedisPool pool = new JedisPool("localhost", 6379);
11 |
12 | @Override
13 | protected void provideActors() {
14 | scheduler = new RedisTaskScheduler(new JedisDriver(pool), taskTriggerListener);
15 | scheduler.setSchedulerName("jedis-scheduler");
16 | scheduler.setClock(clock);
17 | scheduler.setPollingDelayMillis(50);
18 | scheduler.start();
19 | }
20 |
21 | @After
22 | public void stopScheduler() {
23 | scheduler.stop();
24 | }
25 |
26 | @AfterClass
27 | public static void shutdown() {
28 | pool.close();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/test/java/com/github/davidmarquis/redisscheduler/LettuceIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | import com.github.davidmarquis.redisscheduler.drivers.lettuce.LettuceDriver;
4 | import io.lettuce.core.RedisClient;
5 | import io.lettuce.core.RedisURI;
6 | import org.junit.After;
7 | import org.junit.AfterClass;
8 |
9 | public class LettuceIntegrationTest extends AcceptanceTestSuite {
10 |
11 | private static final RedisClient client = RedisClient.create(RedisURI.create("localhost", 6379));
12 |
13 | @Override
14 | protected void provideActors() {
15 | scheduler = new RedisTaskScheduler(new LettuceDriver(client), taskTriggerListener);
16 | scheduler.setSchedulerName("lettuce-scheduler");
17 | scheduler.setClock(clock);
18 | scheduler.setPollingDelayMillis(50);
19 | scheduler.start();
20 | }
21 |
22 | @After
23 | public void stopScheduler() {
24 | scheduler.stop();
25 | }
26 |
27 | @AfterClass
28 | public static void shutdown() {
29 | client.shutdown();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/test/java/com/github/davidmarquis/redisscheduler/RedisTaskSchedulerTest.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 | import org.junit.After;
4 | import org.junit.Before;
5 | import org.junit.Test;
6 | import org.junit.runner.RunWith;
7 | import org.mockito.Mock;
8 | import org.mockito.runners.MockitoJUnitRunner;
9 |
10 | import java.util.function.Function;
11 |
12 | import static org.mockito.Matchers.any;
13 | import static org.mockito.Mockito.*;
14 | import static org.mockito.internal.verification.VerificationModeFactory.atLeast;
15 | import static org.mockito.internal.verification.VerificationModeFactory.times;
16 |
17 | @RunWith(MockitoJUnitRunner.class)
18 | @SuppressWarnings("unchecked")
19 | public class RedisTaskSchedulerTest {
20 |
21 | private static final int MAX_RETRIES = 3;
22 |
23 | @Mock
24 | private RedisDriver driver;
25 |
26 | private RedisTaskScheduler scheduler;
27 |
28 | @Before
29 | public void setUp() {
30 | scheduler = new RedisTaskScheduler(driver, taskId -> {});
31 | scheduler.setPollingDelayMillis(50);
32 | scheduler.setMaxRetriesOnConnectionFailure(MAX_RETRIES);
33 | }
34 |
35 | @After
36 | public void tearDown() {
37 | scheduler.stop();
38 | }
39 |
40 | @Test
41 | public void canRetryAfterRedisConnectionError() throws InterruptedException {
42 | doThrow(RedisConnectException.class).when(driver).fetch(any(Function.class));
43 |
44 | scheduler.start();
45 | Thread.sleep(500);
46 |
47 | verify(driver, times(MAX_RETRIES)).fetch(any(Function.class));
48 | }
49 |
50 | @Test
51 | public void canRecoverAfterSingleConnectionError() throws InterruptedException {
52 | when(driver.fetch(any(Function.class)))
53 | .thenThrow(RedisConnectException.class)
54 | .thenReturn(true);
55 |
56 | scheduler.start();
57 | Thread.sleep(500);
58 |
59 | verify(driver, atLeast(MAX_RETRIES + 1)).fetch( any(Function.class));
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/test/java/com/github/davidmarquis/redisscheduler/SpringIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler;
2 |
3 |
4 | import com.github.davidmarquis.redisscheduler.lib.LatchedTriggerListener;
5 | import com.github.davidmarquis.redisscheduler.lib.StubbedClock;
6 | import org.junit.runner.RunWith;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.context.ApplicationContext;
9 | import org.springframework.test.context.ContextConfiguration;
10 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
11 |
12 | @RunWith(SpringJUnit4ClassRunner.class)
13 | @ContextConfiguration("/application-context-test.xml")
14 | public class SpringIntegrationTest extends AcceptanceTestSuite {
15 |
16 | @Autowired
17 | private ApplicationContext ctx;
18 |
19 | @Override
20 | protected void provideActors() {
21 | scheduler = ctx.getBean(RedisTaskScheduler.class);
22 | clock = ctx.getBean(StubbedClock.class);
23 | taskTriggerListener = ctx.getBean(LatchedTriggerListener.class);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/test/java/com/github/davidmarquis/redisscheduler/lib/LatchedTriggerListener.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler.lib;
2 |
3 | import com.github.davidmarquis.redisscheduler.TaskTriggerListener;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.concurrent.CountDownLatch;
8 | import java.util.concurrent.TimeUnit;
9 |
10 | /**
11 | * Special trigger listener that allows tests to wait until a certain number of tasks have been triggered
12 | * by the scheduler. This is necessary due to the asynchronous nature of the scheduler.
13 | */
14 | public class LatchedTriggerListener implements TaskTriggerListener {
15 |
16 | private CountDownLatch latch;
17 | private List triggeredTasks = new ArrayList<>();
18 |
19 | @Override
20 | public void taskTriggered(String taskId) {
21 | triggeredTasks.add(taskId);
22 |
23 | if (latch != null) {
24 | latch.countDown();
25 | }
26 | }
27 |
28 | public List getTriggeredTasks() {
29 | return triggeredTasks;
30 | }
31 |
32 | public void waitUntilTriggeredCount(int nTimes, int timeoutMillis) throws InterruptedException {
33 | int remainingCount = nTimes - triggeredTasks.size();
34 | if (remainingCount < 1) {
35 | return;
36 | }
37 |
38 | latch = new CountDownLatch(remainingCount);
39 | latch.await(timeoutMillis, TimeUnit.MILLISECONDS);
40 | }
41 |
42 | public void reset() {
43 | triggeredTasks.clear();
44 | latch = null;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/java/com/github/davidmarquis/redisscheduler/lib/StartRedis.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler.lib;
2 |
3 | import org.junit.rules.ExternalResource;
4 | import redis.embedded.RedisServer;
5 |
6 | public class StartRedis extends ExternalResource {
7 |
8 | private RedisServer server;
9 |
10 | @Override
11 | protected void before() throws Throwable {
12 | server = new RedisServer();
13 | server.start();
14 | }
15 |
16 | @Override
17 | protected void after() {
18 | server.stop();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/java/com/github/davidmarquis/redisscheduler/lib/StubbedClock.java:
--------------------------------------------------------------------------------
1 | package com.github.davidmarquis.redisscheduler.lib;
2 |
3 | import java.time.*;
4 | import java.time.format.DateTimeFormatter;
5 | import java.util.concurrent.TimeUnit;
6 |
7 | public class StubbedClock extends Clock {
8 |
9 | private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd HH:mm");
10 |
11 | private Instant now = Instant.now();
12 |
13 | public Instant instant() {
14 | return now;
15 | }
16 |
17 | public void is(String dateTimeStr) {
18 | LocalDateTime time = LocalDateTime.parse(dateTimeStr, FORMATTER);
19 | stubTime(time.atZone(ZoneId.systemDefault()).toInstant());
20 | }
21 |
22 | public void fastForward(int period, TimeUnit unit) {
23 | stubTime(in(period, unit));
24 | }
25 |
26 | public Instant in(int period, TimeUnit unit) {
27 | long currentTime = now.toEpochMilli();
28 | long newTime = currentTime + (unit.toMillis(period));
29 | return Instant.ofEpochMilli(newTime);
30 | }
31 |
32 | private void stubTime(Instant stubbedTime) {
33 | now = stubbedTime;
34 | }
35 |
36 | public ZoneId getZone() {
37 | return null; // not used
38 | }
39 |
40 | public Clock withZone(ZoneId zone) {
41 | return null; // not used
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/test/resources/application-context-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/test/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/test/resources/test-config.properties:
--------------------------------------------------------------------------------
1 | test.redis.host=localhost
2 | test.redis.port=6379
3 | test.redis.database=8
--------------------------------------------------------------------------------