├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── pom.xml
└── src
├── main
└── java
│ └── org
│ └── github
│ └── siahsang
│ └── redutils
│ ├── RedUtilsLock.java
│ ├── RedUtilsLockImpl.java
│ ├── common
│ ├── OperationCallBack.java
│ ├── RedUtilsConfig.java
│ ├── Scheduler.java
│ ├── ThreadManager.java
│ ├── connection
│ │ ├── ConnectionManager.java
│ │ ├── ConnectionPoolFactory.java
│ │ └── JedisConnectionManager.java
│ └── redis
│ │ ├── LuaScript.java
│ │ └── RedisResponse.java
│ ├── exception
│ ├── BadRequestException.java
│ ├── InsufficientResourceException.java
│ ├── RefreshLockException.java
│ └── ReplicaIsDownException.java
│ ├── lock
│ ├── ChannelListener.java
│ ├── JedisChannelListener.java
│ ├── JedisLockChannel.java
│ ├── JedisLockRefresher.java
│ ├── LockChannel.java
│ └── LockRefresher.java
│ └── replica
│ ├── JedisReplicaManager.java
│ └── ReplicaManager.java
└── test
├── java
└── org
│ └── github
│ └── siahsang
│ ├── redutils
│ ├── AbstractBaseTest.java
│ ├── RedUtilsLockImplTest.java
│ └── common
│ │ └── connection
│ │ └── JedisConnectionManagerTest.java
│ └── test
│ └── redis
│ ├── AOFConfiguration.java
│ ├── RedisAddress.java
│ └── RedisServer.java
└── resources
├── Dockerfile
├── docker-entrypoint.sh
├── logback-test.xml
└── simplelogger.properties
/.gitignore:
--------------------------------------------------------------------------------
1 | # Eclipse
2 | .classpath
3 | .project
4 | .settings/
5 |
6 | # Intellij
7 | .idea/
8 | *.iml
9 | *.iws
10 | *.ipr
11 | out/
12 |
13 |
14 | # Crashlytics plugin (for Android Studio and IntelliJ)
15 | com_crashlytics_export_strings.xml
16 | crashlytics.properties
17 | crashlytics-build.properties
18 |
19 | # Mac
20 | .DS_Store
21 |
22 | # Maven
23 | log/
24 | target/
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | jdk: openjdk8
3 | dist: bionic
4 |
5 | script:
6 | - mvn clean package
7 |
8 |
9 | cache:
10 | directories:
11 | - '$HOME/.m2'
12 | - '$HOME/.m2/repository'
13 |
14 | after_success:
15 | - bash <(curl -s https://codecov.io/bash)
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.7
2 |
3 | LABEL maintainer="Javad Alimohammadi "
4 |
5 | # Install system dependencies
6 | RUN apk update
7 | RUN apk add wget
8 | RUN apk add make
9 | RUN apk add gcc
10 | RUN apk add musl-dev
11 | RUN apk add linux-headers
12 | RUN apk add tcl
13 |
14 | # Get last stable redis version
15 | WORKDIR /home
16 | RUN wget -O redis.tar.gz http://download.redis.io/redis-stable.tar.gz
17 | RUN tar xfz redis.tar.gz
18 | RUN rm redis.tar.gz
19 | RUN mv redis-* redis
20 | WORKDIR /home/redis
21 | RUN make
22 | RUN cp /home/redis/src/redis-server /usr/local/bin/
23 | RUN cp /home/redis/src/redis-cli /usr/local/bin/
24 | ENTRYPOINT ["redis-server"]
25 |
--------------------------------------------------------------------------------
/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.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | RedUtils
3 |
4 |
5 | Distributed Lock Implementation With Redis
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## *Note* ##
21 | For reading the idea and the algorithm behind this, please visit [https://dzone.com/articles/distributed-lock-implementation-with-redis](https://dzone.com/articles/distributed-lock-implementation-with-redis
22 | )
23 |
24 | ## Introduction ##
25 | RedUtils is a distributed lock and using Redis for storing and expiring locks. It has the following features:
26 |
27 | - **Leased-Based Lock**: If any clients crash or restarted abnormally, eventually lock will be free.
28 | - **Safe**: Provided that *fsync=always* on every Redis instance we have safety even if Redis become unavailable after getting lock.
29 | - **Auto-Refreshing Lock**: A lock that is acquired by the client can be held as long as the client is alive, and the connection is OK.
30 |
31 |
32 | ## Getting Started ##
33 |
34 | ### Requirements ##
35 | Install Redis or use the following command if you have Docker installed
36 | ```
37 | docker run --name some-redis -e ALLOW_EMPTY_PASSWORD=yes -p 6379:6379 --rm -it redis
38 | ```
39 |
40 | Add the following dependency (Java 8 is required)
41 |
42 | ```
43 |
44 | com.github.siahsang
45 | red-utils
46 | 1.0.4
47 |
48 | ```
49 |
50 |
51 |
52 | ### How to use it? ##
53 |
54 | Getting the lock with the default configuration. Wait for getting the lock if it is acquired by another thread.
55 |
56 | ```
57 | RedUtilsLock redUtilsLock = new RedUtilsLockImpl();
58 | redUtilsLock.acquire("lock1", () -> {
59 | // some operation
60 | });
61 | ```
62 |
63 | Try to acquire the lock and return true after executing the operation, otherwise, return false immediately.
64 | ```
65 | RedUtilsLock redUtilsLock = new RedUtilsLockImpl();
66 | boolean getLockSuccessfully = redUtilsLock.tryAcquire("lock1", () -> {
67 | // some operation
68 | });
69 | ```
70 |
71 | You can also provide configuration when initializing RedUtilsLock
72 | ```
73 | RedUtilsConfig redUtilsConfig = new RedUtilsConfig.RedUtilsConfigBuilder()
74 | .hostAddress("127.0.0.1")
75 | .port("6379")
76 | .replicaCount(3)
77 | .leaseTimeMillis(40_000)
78 | .build();
79 |
80 | RedUtilsLock redUtilsLock = new RedUtilsLockImpl(redUtilsConfig);
81 | ```
82 |
83 | To see more examples please see the tests
84 |
85 |
86 | ### Running the tests ###
87 | For running the tests, you should install Docker(test cases use [testcontainer](https://www.testcontainers.org/) for running Redis).
88 | After that you can run all tests with:
89 | ```
90 | mvn clean test
91 | ```
92 |
93 | ## Caveats ##
94 | There are some caveats that you should be aware of:
95 |
96 | 1. I assume clocks are synchronized between different nodes.
97 | 2. I assume there aren't any long thread pause or process pause after getting lock but before using it.
98 | 3. To achieve strong consistency you should enable the option fsync=always on every Redis instance.
99 | 4. In current implementation, locks is not fair; for example, a client may wait a long time to get the lock and at the same time another client get the lock immediately.
100 |
101 | ### Dependencies ###
102 | - Jedis
103 | - Slf4j
104 |
105 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 |
4 | com.github.siahsang
5 | red-utils
6 | 1.0.5-SNAPSHOT
7 | jar
8 |
9 | Red-Utils
10 | Distributed Lock Implementation With Redis
11 | https://github.com/siahsang/red-utils
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 | j-alimohammadi
24 | Javad Alimohammadi
25 | bs.alimohammadi@gmail.com
26 | https://github.com/siahsang
27 |
28 | developer
29 |
30 |
31 |
32 |
33 |
34 |
35 | scm:git:https://github.com/siahsang/red-utils.git
36 | https://github.com/siahsang/red-utils
37 | scm:git:https://github.com/siahsang/red-utils.git
38 | red-utils-1.0.2
39 |
40 |
41 |
42 |
43 |
44 | ossrh
45 | https://s01.oss.sonatype.org/content/repositories/snapshots
46 |
47 |
48 | ossrh
49 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
50 |
51 |
52 |
53 |
54 | UTF-8
55 | 5.7.0
56 | 1.8
57 | 4.0.3
58 | 1.15.2
59 | 1.15.1
60 | 3.3.0
61 |
62 |
63 |
64 |
65 | redis.clients
66 | jedis
67 | ${jedis.version}
68 |
69 |
70 |
73 |
74 | org.junit.jupiter
75 | junit-jupiter
76 | ${junit.version}
77 | test
78 |
79 |
80 | org.junit.jupiter
81 | junit-jupiter-params
82 | ${junit.version}
83 | test
84 |
85 |
86 |
87 | org.slf4j
88 | slf4j-simple
89 | 1.7.30
90 |
91 |
92 |
93 | org.awaitility
94 | awaitility
95 | ${awaitility.version}
96 | test
97 |
98 |
99 |
100 | org.testcontainers
101 | testcontainers
102 | ${testcontainers.version}
103 | test
104 |
105 |
106 |
107 | org.testcontainers
108 | junit-jupiter
109 | ${junit-jupiter.version}
110 | test
111 |
112 |
113 |
114 |
115 |
116 |
117 | red-utils-${project.version}
118 |
119 |
120 | org.apache.maven.plugins
121 | maven-compiler-plugin
122 | 3.3
123 |
124 | 1.8
125 | 1.8
126 |
127 |
128 |
129 |
130 | org.apache.maven.plugins
131 | maven-surefire-plugin
132 | 3.0.0-M5
133 |
134 |
135 |
136 | org.jacoco
137 | jacoco-maven-plugin
138 | 0.8.6
139 |
140 |
141 |
142 | prepare-agent
143 |
144 |
145 |
146 | report
147 | test
148 |
149 | report
150 |
151 |
152 |
153 |
154 |
155 |
156 | org.apache.maven.plugins
157 | maven-release-plugin
158 | 3.0.0-M1
159 |
160 |
161 | v@{project.version}
162 |
163 |
165 | true
166 | release
167 | false
168 |
171 | release
172 | deploy
173 | -DskipTests
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | release
182 |
183 |
184 |
185 | org.apache.maven.plugins
186 | maven-scm-plugin
187 | 1.11.2
188 |
189 |
190 | org.apache.maven.plugins
191 | maven-gpg-plugin
192 | 1.5
193 |
194 |
195 | sign-artifacts
196 | verify
197 |
198 | sign
199 |
200 |
201 |
202 |
203 |
204 | org.apache.maven.plugins
205 | maven-source-plugin
206 | 2.2.1
207 |
208 |
209 | attach-sources
210 |
211 | jar-no-fork
212 |
213 |
214 |
215 |
216 |
217 | org.apache.maven.plugins
218 | maven-javadoc-plugin
219 | 2.9.1
220 |
221 |
222 | attach-javadocs
223 |
224 | jar
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/RedUtilsLock.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils;
2 |
3 | import org.github.siahsang.redutils.common.OperationCallBack;
4 |
5 | /**
6 | * @author Javad Alimohammadi
7 | */
8 | public interface RedUtilsLock {
9 | /**
10 | * Execute given operation when getting lock successfully from redis.
11 | *
12 | * @param lockName Name of the lock
13 | * @param operationCallBack Operation that should be executed after acquiring lock successfully
14 | * @return True if getting lock successfully, false otherwise
15 | */
16 | boolean tryAcquire(String lockName, OperationCallBack operationCallBack);
17 |
18 | /**
19 | * Execute given operation when getting lock successfully from redis. Wait for getting lock if necessary
20 | *
21 | * @param lockName Name of the lock
22 | * @param operationCallBack Operation that should be executed after acquiring lock successfully
23 | */
24 | void acquire(String lockName, OperationCallBack operationCallBack);
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/RedUtilsLockImpl.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils;
2 |
3 | import org.github.siahsang.redutils.common.OperationCallBack;
4 | import org.github.siahsang.redutils.common.RedUtilsConfig;
5 | import org.github.siahsang.redutils.common.ThreadManager;
6 | import org.github.siahsang.redutils.common.connection.JedisConnectionManager;
7 | import org.github.siahsang.redutils.common.redis.LuaScript;
8 | import org.github.siahsang.redutils.common.redis.RedisResponse;
9 | import org.github.siahsang.redutils.exception.InsufficientResourceException;
10 | import org.github.siahsang.redutils.lock.JedisLockChannel;
11 | import org.github.siahsang.redutils.lock.JedisLockRefresher;
12 | import org.github.siahsang.redutils.lock.LockChannel;
13 | import org.github.siahsang.redutils.lock.LockRefresher;
14 | import org.github.siahsang.redutils.replica.JedisReplicaManager;
15 | import org.github.siahsang.redutils.replica.ReplicaManager;
16 | import org.slf4j.Logger;
17 | import org.slf4j.LoggerFactory;
18 |
19 | import java.util.concurrent.CompletableFuture;
20 | import java.util.concurrent.ExecutorService;
21 | import java.util.concurrent.Executors;
22 |
23 | /**
24 | * @author Javad Alimohammadi
25 | */
26 | public class RedUtilsLockImpl implements RedUtilsLock {
27 | private static final Logger log = LoggerFactory.getLogger(RedUtilsLockImpl.class);
28 |
29 | private final ExecutorService operationExecutorService = Executors.newCachedThreadPool();
30 |
31 | private final LockChannel lockChannel;
32 |
33 | private final ReplicaManager replicaManager;
34 |
35 | private final RedUtilsConfig redUtilsConfig;
36 |
37 | private final JedisConnectionManager connectionManager;
38 |
39 | /**
40 | * Start with default Redis configuration, host:127.0.0.1 and port:6379
41 | */
42 | public RedUtilsLockImpl() {
43 | this(RedUtilsConfig.DEFAULT_HOST_ADDRESS, RedUtilsConfig.DEFAULT_PORT, 0);
44 | }
45 |
46 | public RedUtilsLockImpl(final String hostAddress, final int port) {
47 | this(hostAddress, port, 0);
48 | }
49 |
50 | /**
51 | * Use with master-replica configuration
52 | *
53 | * @param hostAddress server address of Redis
54 | * @param port port number of Redis
55 | * @param replicaCount number of replica
56 | */
57 | public RedUtilsLockImpl(final String hostAddress, final int port, final int replicaCount) {
58 | this(new RedUtilsConfig
59 | .RedUtilsConfigBuilder()
60 | .hostAddress(hostAddress)
61 | .port(port)
62 | .replicaCount(replicaCount)
63 | .build()
64 | );
65 | }
66 |
67 | /**
68 | * To have more control on various configuration use this constructor
69 | * @param redUtilsConfig various configuration parameter that can be set
70 | */
71 | public RedUtilsLockImpl(RedUtilsConfig redUtilsConfig) {
72 | this.redUtilsConfig = redUtilsConfig;
73 | this.connectionManager = new JedisConnectionManager(redUtilsConfig);
74 | this.lockChannel = new JedisLockChannel(connectionManager, redUtilsConfig.getUnlockedMessagePattern());
75 | this.replicaManager = new JedisReplicaManager(connectionManager, redUtilsConfig.getReplicaCount(),
76 | redUtilsConfig.getRetryCountForSyncingWithReplicas(), redUtilsConfig.getWaitingTimeForReplicasMillis());
77 |
78 | }
79 |
80 | @Override
81 | public boolean tryAcquire(final String lockName, final OperationCallBack operationCallBack) {
82 |
83 | if (!connectionManager.reserveOne()) {
84 | throw new InsufficientResourceException("There is`t any available connection, please try again or change connection configs");
85 | }
86 |
87 | boolean getLockSuccessfully = getLock(lockName, redUtilsConfig.getLeaseTimeMillis());
88 | LockRefresher lockRefresher = null;
89 | if (getLockSuccessfully) {
90 | try {
91 | lockRefresher = new JedisLockRefresher(redUtilsConfig, replicaManager, connectionManager);
92 | CompletableFuture lockRefresherFuture = lockRefresher.start(lockName);
93 | CompletableFuture mainOperationFuture = CompletableFuture.runAsync(operationCallBack::doOperation,
94 | operationExecutorService);
95 |
96 | lockRefresherFuture.exceptionally(throwable -> {
97 | mainOperationFuture.completeExceptionally(throwable);
98 | return null;
99 | });
100 |
101 | mainOperationFuture.join();
102 |
103 | } finally {
104 | lockRefresher.tryStop(lockName);
105 | tryReleaseLock(lockName);
106 | tryNotifyOtherClients(lockName);
107 | connectionManager.free();
108 | }
109 |
110 | return true;
111 | }
112 |
113 | return false;
114 | }
115 |
116 | @Override
117 | public void acquire(final String lockName, final OperationCallBack operationCallBack) {
118 | if (!connectionManager.reserve(2)) {
119 | throw new InsufficientResourceException("There is`t any available connection, please try again or change connection configs");
120 | }
121 |
122 | boolean getLockSuccessfully = getLock(lockName, redUtilsConfig.getLeaseTimeMillis());
123 |
124 | if (!getLockSuccessfully) {
125 | try {
126 | lockChannel.subscribe(lockName);
127 |
128 | while (!getLockSuccessfully) {
129 | final long ttl = getTTL(lockName);
130 | if (ttl > 0) {
131 | lockChannel.waitForNotification(lockName, ttl);
132 | } else {
133 | getLockSuccessfully = getLock(lockName, redUtilsConfig.getLeaseTimeMillis());
134 | }
135 | }
136 | } catch (InterruptedException ex) {
137 | Thread.currentThread().interrupt();
138 | throw new IllegalStateException("Interrupted");
139 | } finally {
140 | lockChannel.unSubscribe(lockName);
141 | }
142 | }
143 |
144 | // At this point we have the lock
145 | LockRefresher lockRefresher = new JedisLockRefresher(redUtilsConfig, replicaManager, connectionManager);
146 | try {
147 | CompletableFuture lockRefresherStatus = lockRefresher.start(lockName);
148 | CompletableFuture mainOperationFuture = CompletableFuture.runAsync(operationCallBack::doOperation,
149 | operationExecutorService);
150 |
151 | lockRefresherStatus.exceptionally(throwable -> {
152 | mainOperationFuture.completeExceptionally(throwable);
153 | return null;
154 | });
155 |
156 | mainOperationFuture.join();
157 | } finally {
158 | lockRefresher.tryStop(lockName);
159 | tryReleaseLock(lockName);
160 | tryNotifyOtherClients(lockName);
161 | connectionManager.free();
162 | }
163 | }
164 |
165 |
166 | private boolean getLock(final String lockName, final long expirationTimeMillis) {
167 |
168 | final String lockValue = ThreadManager.getName();
169 |
170 | try {
171 | Object response = connectionManager.doWithConnection(jedis -> {
172 | return jedis.eval(LuaScript.GET_LOCK, 1, lockName, lockValue, String.valueOf(expirationTimeMillis));
173 | });
174 | if (RedisResponse.isFailed(response)) {
175 | return false;
176 | }
177 | replicaManager.waitForResponse();
178 | return true;
179 | } catch (Exception exception) {
180 | releaseLock(lockName);
181 | throw exception;
182 | }
183 |
184 | }
185 |
186 | private void releaseLock(String lockName) {
187 | String lockValue = ThreadManager.getName();
188 | connectionManager.doWithConnection(jedis -> {
189 | return jedis.eval(LuaScript.RELEASE_LOCK, 1, lockName, lockValue);
190 | });
191 |
192 | }
193 |
194 | private void tryReleaseLock(String lockName) {
195 | try {
196 | releaseLock(lockName);
197 | } catch (Exception ex) {
198 | log.debug("Could not release lock [{}]", lockName, ex);
199 | }
200 | }
201 |
202 | private long getTTL(final String lockName) {
203 | return connectionManager.doWithConnection(jedis -> jedis.pttl(lockName));
204 | }
205 |
206 |
207 | public void tryNotifyOtherClients(final String lockName) {
208 | try {
209 | connectionManager.doWithConnection(jedis -> {
210 | return jedis.publish(lockName, redUtilsConfig.getUnlockedMessagePattern());
211 | });
212 | } catch (Exception exception) {
213 | // nothing
214 | log.debug("Error in notify [{}] to other clients", lockName, exception);
215 | }
216 | }
217 |
218 | }
219 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/common/OperationCallBack.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common;
2 |
3 | /**
4 | * @author Javad Alimohammadi
5 | */
6 |
7 | @FunctionalInterface
8 | public interface OperationCallBack {
9 | void doOperation();
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/common/RedUtilsConfig.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common;
2 |
3 | /**
4 | * @author Javad Alimohammadi
5 | */
6 |
7 | public class RedUtilsConfig {
8 | public static final String DEFAULT_HOST_ADDRESS = "127.0.0.1";
9 |
10 | public static final int DEFAULT_PORT = 6379;
11 |
12 | private final int waitingTimeForReplicasMillis;
13 |
14 | private final int retryCountForSyncingWithReplicas;
15 |
16 | private final int leaseTimeMillis;
17 |
18 | private final int readTimeOutMillis;
19 |
20 | private final int lockMaxPoolSize;
21 |
22 | private final String unlockedMessagePattern;
23 |
24 | private final int replicaCount;
25 |
26 | private final String hostAddress;
27 |
28 | public final int port;
29 |
30 | private RedUtilsConfig(RedUtilsConfigBuilder redUtilsConfigBuilder) {
31 | this.waitingTimeForReplicasMillis = redUtilsConfigBuilder.waitingTimeForReplicasMillis;
32 | this.retryCountForSyncingWithReplicas = redUtilsConfigBuilder.retryCountForSyncingWithReplicas;
33 | this.leaseTimeMillis = redUtilsConfigBuilder.leaseTimeMillis;
34 | this.readTimeOutMillis = redUtilsConfigBuilder.readTimeOutMillis;
35 | this.lockMaxPoolSize = redUtilsConfigBuilder.maxPoolSize;
36 | this.unlockedMessagePattern = redUtilsConfigBuilder.redUtilsUnLockedMessage;
37 | this.replicaCount = redUtilsConfigBuilder.replicaCount;
38 | this.hostAddress = redUtilsConfigBuilder.hostAddress;
39 | this.port = redUtilsConfigBuilder.port;
40 |
41 | }
42 |
43 | public int getWaitingTimeForReplicasMillis() {
44 | return waitingTimeForReplicasMillis;
45 | }
46 |
47 | public int getRetryCountForSyncingWithReplicas() {
48 | return retryCountForSyncingWithReplicas;
49 | }
50 |
51 | public int getLeaseTimeMillis() {
52 | return leaseTimeMillis;
53 | }
54 |
55 | public int getReadTimeOutMillis() {
56 | return readTimeOutMillis;
57 | }
58 |
59 | public int getLockMaxPoolSize() {
60 | return lockMaxPoolSize;
61 | }
62 |
63 | public String getUnlockedMessagePattern() {
64 | return unlockedMessagePattern;
65 | }
66 |
67 | public int getReplicaCount() {
68 | return replicaCount;
69 | }
70 |
71 | public String getHostAddress() {
72 | return hostAddress;
73 | }
74 |
75 | public int getPort() {
76 | return port;
77 | }
78 |
79 |
80 | public static final class RedUtilsConfigBuilder {
81 | private int waitingTimeForReplicasMillis = 1000;
82 |
83 | private int retryCountForSyncingWithReplicas = 3;
84 |
85 | private int leaseTimeMillis = 30_000;
86 |
87 | private int readTimeOutMillis = 2000;
88 |
89 | private int maxPoolSize = 60;
90 |
91 | private String redUtilsUnLockedMessage = "RED_UTILS_UN_LOCKED_";
92 |
93 | private int replicaCount = 0;
94 |
95 | private String hostAddress = DEFAULT_HOST_ADDRESS;
96 |
97 | private int port = DEFAULT_PORT;
98 |
99 | public RedUtilsConfig build() {
100 | return new RedUtilsConfig(this);
101 | }
102 |
103 | public RedUtilsConfigBuilder waitingTimeForReplicasMillis(int waitingTimeForReplicasMillis) {
104 | this.waitingTimeForReplicasMillis = waitingTimeForReplicasMillis;
105 | return this;
106 | }
107 |
108 | public RedUtilsConfigBuilder retryCountForSyncingWithReplicas(int retryCountForSyncingWithReplicas) {
109 | this.retryCountForSyncingWithReplicas = retryCountForSyncingWithReplicas;
110 | return this;
111 | }
112 |
113 | public RedUtilsConfigBuilder leaseTimeMillis(int leaseTimeMillis) {
114 | this.leaseTimeMillis = leaseTimeMillis;
115 | return this;
116 | }
117 |
118 | public RedUtilsConfigBuilder readTimeOutMillis(int readTimeOutMillis) {
119 | this.readTimeOutMillis = readTimeOutMillis;
120 | return this;
121 | }
122 |
123 | public RedUtilsConfigBuilder maxPoolSize(int lockMaxPoolSize) {
124 | this.maxPoolSize = lockMaxPoolSize;
125 | return this;
126 | }
127 |
128 | public RedUtilsConfigBuilder redUtilsUnLockedMessage(String redUtilsUnLockedMessage) {
129 | this.redUtilsUnLockedMessage = redUtilsUnLockedMessage;
130 | return this;
131 | }
132 |
133 | public RedUtilsConfigBuilder replicaCount(int replicaCount) {
134 | this.replicaCount = replicaCount;
135 | return this;
136 | }
137 |
138 | public RedUtilsConfigBuilder hostAddress(String hostAddress) {
139 | this.hostAddress = hostAddress;
140 | return this;
141 | }
142 |
143 | public RedUtilsConfigBuilder port(int port) {
144 | this.port = port;
145 | return this;
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/common/Scheduler.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common;
2 |
3 | import java.util.concurrent.CompletableFuture;
4 | import java.util.concurrent.ScheduledExecutorService;
5 | import java.util.concurrent.TimeUnit;
6 |
7 | /**
8 | * @author Javad Alimohammadi
9 | */
10 |
11 | public abstract class Scheduler {
12 |
13 | private Scheduler() {
14 | }
15 |
16 | public static CompletableFuture scheduleAtFixRate(ScheduledExecutorService executor, OperationCallBack operationCallBack,
17 | final long initialDelay, final long delay, final TimeUnit unit) {
18 | CompletableFuture completableFuture = new CompletableFuture<>();
19 |
20 | executor.scheduleAtFixedRate(() -> {
21 | try {
22 | operationCallBack.doOperation();
23 | } catch (Exception exception) {
24 | completableFuture.completeExceptionally(exception);
25 | throw exception;
26 | }
27 |
28 | }, initialDelay, delay, unit);
29 |
30 | return completableFuture;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/common/ThreadManager.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common;
2 |
3 | import java.util.UUID;
4 |
5 | /**
6 | * @author Javad Alimohammadi
7 | */
8 | public abstract class ThreadManager {
9 | private ThreadManager() {
10 | }
11 |
12 | private static final InheritableThreadLocal PARENT_THREAD_NAME = new InheritableThreadLocal<>();
13 |
14 | private static final String GENERATED_UUID = UUID.randomUUID().toString();
15 |
16 | public static String createUniqiueName() {
17 | long threadId = Thread.currentThread().getId();
18 | PARENT_THREAD_NAME.set(threadId + ":" + GENERATED_UUID);
19 | return PARENT_THREAD_NAME.get();
20 | }
21 |
22 | public static String getName() {
23 | return PARENT_THREAD_NAME.get();
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/common/connection/ConnectionManager.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common.connection;
2 |
3 | import redis.clients.jedis.Jedis;
4 |
5 | import java.util.function.Function;
6 |
7 | /**
8 | * @author Javad Alimohammadi
9 | */
10 | public interface ConnectionManager {
11 | boolean reserve(String resourceId, int size);
12 |
13 | /**
14 | * First close all connection and then remove the resource id
15 | * @param resourceId Client id that request for getting connection
16 | */
17 | void free(String resourceId);
18 |
19 |
20 | void free();
21 |
22 | boolean reserve(int size);
23 |
24 | boolean reserveOne();
25 |
26 | T borrow(String resourceId);
27 |
28 | T borrow();
29 |
30 | void returnBack(String resourceId, T connection);
31 |
32 | void returnBack(T connection);
33 |
34 | E doWithConnection(String resourceId, Function operation);
35 |
36 | E doWithConnection(Function operation);
37 |
38 | int remainingCapacity();
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/common/connection/ConnectionPoolFactory.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common.connection;
2 |
3 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
4 | import redis.clients.jedis.Jedis;
5 |
6 | /**
7 | * @author Javad Alimohammadi
8 | */
9 |
10 | public abstract class ConnectionPoolFactory {
11 | private ConnectionPoolFactory() {
12 | }
13 |
14 | public static GenericObjectPoolConfig makePool(final int maxSize) {
15 | GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig<>();
16 | poolConfig.setTestWhileIdle(true);
17 | poolConfig.setMinEvictableIdleTimeMillis(60000);
18 | poolConfig.setTimeBetweenEvictionRunsMillis(30000);
19 | poolConfig.setNumTestsPerEvictionRun(-1);
20 |
21 | poolConfig.setMaxTotal(maxSize);
22 |
23 | return poolConfig;
24 | }
25 |
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/common/connection/JedisConnectionManager.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common.connection;
2 |
3 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
4 | import org.github.siahsang.redutils.common.RedUtilsConfig;
5 | import org.github.siahsang.redutils.common.ThreadManager;
6 | import org.github.siahsang.redutils.exception.BadRequestException;
7 | import org.github.siahsang.redutils.exception.InsufficientResourceException;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import redis.clients.jedis.Jedis;
11 | import redis.clients.jedis.JedisPool;
12 |
13 | import java.util.ArrayList;
14 | import java.util.List;
15 | import java.util.Map;
16 | import java.util.Objects;
17 | import java.util.concurrent.ConcurrentHashMap;
18 | import java.util.concurrent.atomic.AtomicBoolean;
19 | import java.util.concurrent.atomic.AtomicInteger;
20 | import java.util.function.Function;
21 |
22 | /**
23 | * @author Javad Alimohammadi
24 | */
25 |
26 | public class JedisConnectionManager implements ConnectionManager {
27 | private static final Logger log = LoggerFactory.getLogger(JedisConnectionManager.class);
28 |
29 | private final AtomicInteger capacity = new AtomicInteger(0);
30 |
31 | private final Map> reservedConnections = new ConcurrentHashMap<>();
32 |
33 |
34 | private final JedisPool channelConnectionPool;
35 |
36 | public JedisConnectionManager(RedUtilsConfig redUtilsConfig) {
37 | this.capacity.set(redUtilsConfig.getLockMaxPoolSize());
38 |
39 | GenericObjectPoolConfig lockPoolConfig = ConnectionPoolFactory.makePool(capacity.get());
40 | this.channelConnectionPool = new JedisPool(lockPoolConfig,
41 | redUtilsConfig.getHostAddress(),
42 | redUtilsConfig.getPort(),
43 | redUtilsConfig.getReadTimeOutMillis()
44 | );
45 | }
46 |
47 | @Override
48 | public boolean reserve(final String resourceId, final int size) {
49 | AtomicBoolean reservedSuccessfully = new AtomicBoolean(false);
50 | capacity.updateAndGet(operand -> {
51 | if (operand - size >= 0) {
52 | reservedSuccessfully.set(true);
53 | return operand - size;
54 | } else {
55 | reservedSuccessfully.set(false);
56 | return operand;
57 | }
58 | });
59 |
60 | if (reservedSuccessfully.get()) {
61 | reservedConnections.putIfAbsent(resourceId, new ArrayList<>());
62 | for (int i = 0; i < size; i++) {
63 | Jedis resource = channelConnectionPool.getResource();
64 | reservedConnections.get(resourceId).add(resource);
65 | log.trace("Reserved connection with resource_id [{}] successfully.", resourceId);
66 | }
67 | }
68 |
69 | return reservedSuccessfully.get();
70 | }
71 |
72 | @Override
73 | public boolean reserve(final int size) {
74 | String connectionId = ThreadManager.createUniqiueName();
75 | return reserve(connectionId, size);
76 | }
77 |
78 |
79 | @Override
80 | public boolean reserveOne() {
81 | String connectionId = ThreadManager.createUniqiueName();
82 | return reserve(connectionId, 1);
83 | }
84 |
85 | @Override
86 | public Jedis borrow(final String resourceId) {
87 | List returnList = new ArrayList<>();
88 | reservedConnections.compute(resourceId, (s, jedisList) -> {
89 | if (Objects.isNull(jedisList)) {
90 | throw new BadRequestException(invalidResourceIdMessage(resourceId));
91 | }
92 |
93 | if (jedisList.isEmpty()) {
94 | throw new InsufficientResourceException("There is no any free connection. Try later!");
95 | }
96 |
97 | Jedis jedis = jedisList.remove(jedisList.size() - 1);
98 | returnList.add(jedis);
99 |
100 | return jedisList;
101 | });
102 |
103 | return returnList.get(0);
104 | }
105 |
106 | @Override
107 | public Jedis borrow() {
108 | String connectionId = ThreadManager.getName();
109 | return borrow(connectionId);
110 | }
111 |
112 | @Override
113 | public void returnBack(final String resourceId, final Jedis connection) {
114 | reservedConnections.compute(resourceId, (s, jedisList) -> {
115 | if (Objects.isNull(jedisList)) {
116 | throw new BadRequestException(invalidResourceIdMessage(resourceId));
117 | }
118 | jedisList.add(connection);
119 | return jedisList;
120 | });
121 |
122 | capacity.incrementAndGet();
123 | }
124 |
125 | @Override
126 | public void returnBack(Jedis connection) {
127 | String connectionId = ThreadManager.getName();
128 | returnBack(connectionId, connection);
129 | }
130 |
131 |
132 | @Override
133 | public E doWithConnection(String resourceId, Function operation) {
134 | Jedis jedis = borrow(resourceId);
135 | try {
136 | return operation.apply(jedis);
137 | } finally {
138 | returnBack(resourceId, jedis);
139 | }
140 | }
141 |
142 | @Override
143 | public E doWithConnection(Function operation) {
144 | String connectionId = ThreadManager.getName();
145 | return doWithConnection(connectionId, operation);
146 | }
147 |
148 | @Override
149 | public int remainingCapacity() {
150 | return capacity.get();
151 | }
152 |
153 |
154 | @Override
155 | public void free(String resourceId) {
156 | reservedConnections.compute(resourceId, (s, jedisList) -> {
157 | if (Objects.isNull(jedisList)) {
158 | throw new BadRequestException(invalidResourceIdMessage(resourceId));
159 | }
160 |
161 | if (!jedisList.isEmpty()) {
162 | jedisList.forEach(Jedis::close);
163 | }
164 |
165 | jedisList.clear();
166 | log.debug("Free connections for resource_id [{}] successfully", resourceId);
167 | return null;
168 | });
169 | }
170 |
171 | @Override
172 | public void free() {
173 | String connectionId = ThreadManager.getName();
174 | free(connectionId);
175 | }
176 |
177 |
178 | private String invalidResourceIdMessage(String resourceId) {
179 | return String.format("Invalid resource_id %s", resourceId);
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/common/redis/LuaScript.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common.redis;
2 |
3 | /**
4 | * @author Javad Alimohammadi
5 | */
6 |
7 | public final class LuaScript {
8 | private LuaScript() {
9 | }
10 |
11 | // @formatter:off
12 | public static final String GET_LOCK = String.format(
13 | "if redis.call('EXISTS', KEYS[1]) == 0 then " +
14 | " redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) " +
15 | " return '%s'" +
16 | "elseif redis.call('GET', KEYS[1]) == ARGV[1] then " +
17 | " redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
18 | " return '%s' " +
19 | "else " +
20 | " return '%s' "+
21 | "end ", RedisResponse.SUCCESS, RedisResponse.SUCCESS, RedisResponse.FAIL);
22 |
23 |
24 |
25 |
26 | public static final String RELEASE_LOCK = String.format(
27 | "if redis.call('get',KEYS[1]) == ARGV[1] then " +
28 | " redis.call('del', KEYS[1]) " +
29 | " return '%s' " +
30 | "else " +
31 | " return '%s' " +
32 | "end", RedisResponse.SUCCESS, RedisResponse.FAIL);
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/common/redis/RedisResponse.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common.redis;
2 |
3 | import java.util.Objects;
4 |
5 | /**
6 | * @author Javad Alimohammadi
7 | */
8 |
9 | public enum RedisResponse {
10 | /**
11 | * Ok response from redis
12 | */
13 | SUCCESS("SUCCESS"),
14 | /**
15 | * NIL response from redis
16 | */
17 | FAIL("FAIL");
18 |
19 | final String val;
20 |
21 | RedisResponse(String val) {
22 | this.val = val;
23 | }
24 |
25 | public static boolean isSuccessFull(final String response) {
26 | return SUCCESS.val.equalsIgnoreCase(response);
27 | }
28 |
29 | public static boolean isFailed(Object response) {
30 | return Objects.equals(RedisResponse.FAIL.val, response);
31 | }
32 |
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/exception/BadRequestException.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.exception;
2 |
3 | /**
4 | * @author Javad Alimohammadi
5 | */
6 | public class BadRequestException extends RuntimeException {
7 | public BadRequestException(String message) {
8 | super(message);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/exception/InsufficientResourceException.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.exception;
2 |
3 | /**
4 | * @author Javad Alimohammadi
5 | */
6 | public class InsufficientResourceException extends RuntimeException {
7 | public InsufficientResourceException(String message) {
8 | super(message);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/exception/RefreshLockException.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.exception;
2 |
3 | /**
4 | * @author Javad Alimohammadi
5 | */
6 |
7 | public class RefreshLockException extends RuntimeException {
8 | public RefreshLockException(String msg, Throwable cause) {
9 | super(msg,cause);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/exception/ReplicaIsDownException.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.exception;
2 |
3 | /**
4 | * @author Javad Alimohammadi
5 | */
6 |
7 | public class ReplicaIsDownException extends RuntimeException {
8 | public ReplicaIsDownException(String message) {
9 | super(message);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/lock/ChannelListener.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.lock;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 | import java.util.concurrent.Semaphore;
6 | import java.util.concurrent.TimeUnit;
7 | import java.util.concurrent.atomic.AtomicBoolean;
8 |
9 | /**
10 | * This class hold information about subscribers for the channel
11 | *
12 | * @author Javad Alimohammadi
13 | */
14 | public abstract class ChannelListener {
15 |
16 | private final AtomicBoolean hasNotification = new AtomicBoolean(false);
17 |
18 | private final Semaphore notificationResource = new Semaphore(0);
19 |
20 | private final Set subscribers = new HashSet<>();
21 |
22 |
23 | /**
24 | * Wait calling thread for getting notification from channel.
25 | *
26 | * @param timeOutMillis maximum amount of time for waiting to get
27 | * @throws InterruptedException
28 | */
29 | public void waitForGettingNotificationFromChannel(final long timeOutMillis) throws InterruptedException {
30 |
31 | boolean acquireNotificationRecourse = notificationResource.tryAcquire(timeOutMillis, TimeUnit.MILLISECONDS);
32 |
33 | // if true, this means we got a new message from the channel and we consumed it
34 | if (acquireNotificationRecourse) {
35 | hasNotification.set(false);
36 | }
37 |
38 | }
39 |
40 | public void onGettingNewMessage() {
41 | if (hasNotification.compareAndSet(false, true)) {
42 | notificationResource.release();
43 | }
44 | }
45 |
46 | public void addSubscriber(long subscriberId) {
47 | subscribers.add(subscriberId);
48 | }
49 |
50 | public void removeSubscriber(long subscriberId) {
51 | subscribers.remove(subscriberId);
52 | }
53 |
54 | public boolean isSubscribersEmpty() {
55 | return subscribers.isEmpty();
56 | }
57 |
58 | public abstract void shutdown();
59 |
60 | public abstract void startListening();
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/lock/JedisChannelListener.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.lock;
2 |
3 | import org.github.siahsang.redutils.common.connection.ConnectionManager;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import redis.clients.jedis.Jedis;
7 | import redis.clients.jedis.JedisPubSub;
8 |
9 | import java.util.concurrent.ExecutorService;
10 | import java.util.concurrent.Executors;
11 |
12 | /**
13 | * @author Javad Alimohammadi
14 | */
15 | public class JedisChannelListener extends ChannelListener {
16 | private final Logger log = LoggerFactory.getLogger(JedisChannelListener.class);
17 |
18 | private final String unlockedMessagePattern;
19 |
20 | private JedisPubSub jedisPubSub;
21 |
22 | private final ExecutorService executorService = Executors.newSingleThreadExecutor();
23 |
24 | private final String channelName;
25 |
26 | private final ConnectionManager jedisConnectionManager;
27 |
28 | private Jedis jedis;
29 |
30 | public JedisChannelListener(String unlockedMessagePattern, String channelName, ConnectionManager jedisConnectionManager) {
31 | this.unlockedMessagePattern = unlockedMessagePattern;
32 | this.channelName = channelName;
33 | this.jedisConnectionManager = jedisConnectionManager;
34 |
35 | }
36 |
37 | @Override
38 | public void shutdown() {
39 | try {
40 | jedisPubSub.unsubscribe();
41 | } catch (Exception exception) {
42 | log.debug("Error in unsubscribing channel [{}]", channelName);
43 | }
44 |
45 | try {
46 | executorService.shutdownNow();
47 | } catch (Exception ex) {
48 | log.debug("Error in shut-down channel [{}]", channelName);
49 | }
50 |
51 | jedisConnectionManager.returnBack(jedis);
52 | }
53 |
54 | @Override
55 | public void startListening() {
56 | jedisPubSub = new JedisPubSub() {
57 | @Override
58 | public void onMessage(String channel, String message) {
59 | if (message.startsWith(unlockedMessagePattern)) {
60 | onGettingNewMessage();
61 | }
62 | }
63 | };
64 |
65 | jedis = jedisConnectionManager.borrow();
66 | executorService.submit(() -> {
67 | jedis.subscribe(jedisPubSub, channelName);
68 | });
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/lock/JedisLockChannel.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.lock;
2 |
3 | import org.github.siahsang.redutils.common.connection.ConnectionManager;
4 | import org.github.siahsang.redutils.common.connection.JedisConnectionManager;
5 | import redis.clients.jedis.Jedis;
6 |
7 | import java.util.concurrent.ConcurrentHashMap;
8 |
9 | /**
10 | * @author Javad Alimohammadi
11 | */
12 |
13 | public class JedisLockChannel implements LockChannel {
14 |
15 | private final ConcurrentHashMap lockNameChannelInfo = new ConcurrentHashMap<>();
16 |
17 | private final String unlockedMessagePattern;
18 |
19 | private final ConnectionManager jedisConnectionManager;
20 |
21 | public JedisLockChannel(JedisConnectionManager jedisConnectionManager, final String unlockedMessagePattern) {
22 | this.jedisConnectionManager = jedisConnectionManager;
23 | this.unlockedMessagePattern = unlockedMessagePattern;
24 | }
25 |
26 | @Override
27 | public void subscribe(final String lockName) {
28 | lockNameChannelInfo.compute(lockName, (s, channelListener) -> {
29 | final long threadId = Thread.currentThread().getId();
30 | if (channelListener == null) {
31 | channelListener = new JedisChannelListener(unlockedMessagePattern, lockName, jedisConnectionManager);
32 | channelListener.startListening();
33 | }
34 |
35 | channelListener.addSubscriber(threadId);
36 | return channelListener;
37 | });
38 | }
39 |
40 | @Override
41 | public void waitForNotification(final String lockName, final long timeOutMillis) throws InterruptedException {
42 | lockNameChannelInfo.compute(lockName, (lname, redisChannel) -> {
43 | if (redisChannel == null) {
44 | throw new IllegalArgumentException("There isn`t any channel with name " + lockName);
45 | }
46 | return redisChannel;
47 | });
48 |
49 | lockNameChannelInfo.get(lockName).waitForGettingNotificationFromChannel(timeOutMillis);
50 | }
51 |
52 | @Override
53 | public void unSubscribe(final String lockName) {
54 | lockNameChannelInfo.compute(lockName, (lock, redisChannel) -> {
55 | final long threadId = Thread.currentThread().getId();
56 |
57 | if (redisChannel == null) {
58 | throw new IllegalArgumentException("There isn`t any channel with name " + lockName);
59 | }
60 | // if all subscriber removed, it means we do not need to preserve channel
61 | redisChannel.removeSubscriber(threadId);
62 | if (redisChannel.isSubscribersEmpty()) {
63 | redisChannel.shutdown();
64 | return null;
65 | }
66 |
67 | return redisChannel;
68 | });
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/lock/JedisLockRefresher.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.lock;
2 |
3 | import org.github.siahsang.redutils.common.RedUtilsConfig;
4 | import org.github.siahsang.redutils.common.Scheduler;
5 | import org.github.siahsang.redutils.common.connection.ConnectionManager;
6 | import org.github.siahsang.redutils.exception.RefreshLockException;
7 | import org.github.siahsang.redutils.replica.ReplicaManager;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import redis.clients.jedis.Jedis;
11 |
12 | import java.util.concurrent.CompletableFuture;
13 | import java.util.concurrent.Executors;
14 | import java.util.concurrent.ScheduledExecutorService;
15 | import java.util.concurrent.TimeUnit;
16 |
17 | /**
18 | * @author Javad Alimohammadi
19 | */
20 |
21 | public class JedisLockRefresher implements LockRefresher {
22 | private static final Logger log = LoggerFactory.getLogger(JedisLockRefresher.class);
23 |
24 | private final RedUtilsConfig redUtilsConfig;
25 |
26 | private final ReplicaManager replicaManager;
27 |
28 | private final ConnectionManager jedisConnectionManager;
29 |
30 | private final ScheduledExecutorService executor;
31 |
32 | public JedisLockRefresher(RedUtilsConfig redUtilsConfig, ReplicaManager replicaManager,
33 | ConnectionManager jedisConnectionManager) {
34 | this.redUtilsConfig = redUtilsConfig;
35 | this.replicaManager = replicaManager;
36 | this.jedisConnectionManager = jedisConnectionManager;
37 | this.executor = Executors.newScheduledThreadPool(1);
38 | }
39 |
40 | @Override
41 | public CompletableFuture start(final String lockName) {
42 | final int refreshPeriodMillis = redUtilsConfig.getLeaseTimeMillis();
43 |
44 | return Scheduler.scheduleAtFixRate(executor, () -> {
45 | try {
46 | log.trace("Refreshing the lock [{}]", lockName);
47 | jedisConnectionManager.doWithConnection(jedis -> {
48 | return jedis.pexpire(lockName, refreshPeriodMillis);
49 | });
50 | replicaManager.waitForResponse();
51 | } catch (Exception ex) {
52 | String errMSG = String.format("Error in refreshing the lock '%s'", lockName);
53 | throw new RefreshLockException(errMSG, ex);
54 | }
55 | }, refreshPeriodMillis / 3, refreshPeriodMillis / 3, TimeUnit.MILLISECONDS);
56 | }
57 |
58 | @Override
59 | public void tryStop(final String lockName) {
60 | try {
61 | executor.shutdownNow();
62 | } catch (Exception exception) {
63 | log.debug("Error in stopping Refresher", exception);
64 | }
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/lock/LockChannel.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.lock;
2 |
3 | /**
4 | * @author Javad Alimohammadi
5 | */
6 | public interface LockChannel {
7 | void subscribe(String lockName);
8 |
9 | /**
10 | * Wait for getting the notification of releasing the lock
11 | *
12 | * @param lockName
13 | * @param timeOutMillis
14 | * @throws InterruptedException
15 | */
16 | void waitForNotification(String lockName, long timeOutMillis) throws InterruptedException;
17 |
18 | void unSubscribe(String lockName);
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/lock/LockRefresher.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.lock;
2 |
3 | import java.util.concurrent.CompletableFuture;
4 |
5 | /**
6 | * @author Javad Alimohammadi
7 | */
8 | public interface LockRefresher {
9 | CompletableFuture start(String lockName);
10 |
11 | void tryStop(String lockName);
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/replica/JedisReplicaManager.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.replica;
2 |
3 | import org.github.siahsang.redutils.common.connection.JedisConnectionManager;
4 | import org.github.siahsang.redutils.exception.ReplicaIsDownException;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | /**
9 | * @author Javad Alimohammadi
10 | */
11 |
12 | public class JedisReplicaManager implements ReplicaManager {
13 | private static final Logger log = LoggerFactory.getLogger(JedisReplicaManager.class);
14 |
15 | private final JedisConnectionManager jedisConnectionManager;
16 |
17 | private final int replicaCount;
18 |
19 | private final int retryCount;
20 |
21 | private final int waitingTimeMillis;
22 |
23 | public JedisReplicaManager(JedisConnectionManager jedisConnectionManager, int replicaCount,
24 | int retryCount, int waitingTimeMillis) {
25 | this.jedisConnectionManager = jedisConnectionManager;
26 | this.replicaCount = replicaCount;
27 | this.retryCount = retryCount;
28 | this.waitingTimeMillis = waitingTimeMillis;
29 | }
30 |
31 | @Override
32 | public void waitForResponse() {
33 | if (replicaCount > 0) {
34 | int retry = 1;
35 | long replicaResponseCount = jedisConnectionManager.doWithConnection(jedis -> {
36 | return jedis.waitReplicas(replicaCount, waitingTimeMillis);
37 | });
38 |
39 |
40 | while (replicaResponseCount != replicaCount && retry <= retryCount) {
41 | log.warn("Expected number of replica(s) is [{}] but available number of replica(s) is [{}], trying again({})",
42 | replicaCount, replicaResponseCount, retry);
43 | replicaResponseCount = jedisConnectionManager.doWithConnection(jedis -> {
44 | return jedis.waitReplicas(replicaCount, waitingTimeMillis);
45 | });
46 | retry++;
47 | }
48 |
49 | if (retry > retryCount) {
50 | String msg = String.format("Expected number of replica(s) is [%s] but available number of replica(s) is [%s]",
51 | replicaCount, replicaResponseCount);
52 | throw new ReplicaIsDownException(msg);
53 | }
54 | }
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/org/github/siahsang/redutils/replica/ReplicaManager.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.replica;
2 |
3 | /**
4 | * @author Javad Alimohammadi
5 | */
6 | public interface ReplicaManager {
7 | void waitForResponse();
8 | }
9 |
--------------------------------------------------------------------------------
/src/test/java/org/github/siahsang/redutils/AbstractBaseTest.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils;
2 |
3 | import java.util.concurrent.TimeUnit;
4 |
5 | /**
6 | * @author Javad Alimohammadi
7 | */
8 |
9 | public abstract class AbstractBaseTest {
10 | protected void sleepSeconds(final long seconds) {
11 | try {
12 | TimeUnit.SECONDS.sleep(seconds);
13 | } catch (InterruptedException e) {
14 | e.printStackTrace();
15 | }
16 | }
17 |
18 | protected void sleepMinutes(final long minutes) {
19 | try {
20 | TimeUnit.MINUTES.sleep(minutes);
21 | } catch (InterruptedException e) {
22 | e.printStackTrace();
23 | }
24 | }
25 |
26 | protected void sleepMillis(final long millis) {
27 | try {
28 | TimeUnit.MILLISECONDS.sleep(millis);
29 | } catch (InterruptedException e) {
30 | e.printStackTrace();
31 | }
32 | }
33 |
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/test/java/org/github/siahsang/redutils/RedUtilsLockImplTest.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils;
2 |
3 | import org.awaitility.Awaitility;
4 | import org.github.siahsang.redutils.common.RedUtilsConfig;
5 | import org.github.siahsang.redutils.exception.RefreshLockException;
6 | import org.github.siahsang.redutils.exception.ReplicaIsDownException;
7 | import org.github.siahsang.test.redis.RedisAddress;
8 | import org.github.siahsang.test.redis.RedisServer;
9 | import org.junit.jupiter.api.AfterEach;
10 | import org.junit.jupiter.api.Assertions;
11 | import org.junit.jupiter.api.Disabled;
12 | import org.junit.jupiter.api.Test;
13 | import org.testcontainers.junit.jupiter.Testcontainers;
14 | import redis.clients.jedis.Jedis;
15 | import redis.clients.jedis.exceptions.JedisConnectionException;
16 |
17 | import java.time.Duration;
18 | import java.util.ArrayList;
19 | import java.util.List;
20 | import java.util.concurrent.CompletableFuture;
21 | import java.util.concurrent.ExecutorService;
22 | import java.util.concurrent.Executors;
23 | import java.util.concurrent.TimeUnit;
24 | import java.util.concurrent.atomic.AtomicBoolean;
25 | import java.util.concurrent.atomic.AtomicInteger;
26 | import java.util.concurrent.atomic.AtomicReference;
27 |
28 | /**
29 | * @author Javad Alimohammadi
30 | */
31 |
32 | @Testcontainers
33 | class RedUtilsLockImplTest extends AbstractBaseTest {
34 |
35 | private final static RedisAddress GENERAL_REDIS_ADDRESS = new RedisServer().startSingleInstance();
36 |
37 | private final static Jedis JEDIS = new Jedis(GENERAL_REDIS_ADDRESS.masterHostAddress, GENERAL_REDIS_ADDRESS.masterPort);
38 |
39 | @AfterEach
40 | void afterEach() {
41 | JEDIS.flushAll();
42 | }
43 |
44 | @Test
45 | void test_tryAcquire_WHEN_happy_path() throws Exception {
46 | //************************
47 | // Given
48 | //************************
49 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(GENERAL_REDIS_ADDRESS.masterHostAddress, GENERAL_REDIS_ADDRESS.masterPort);
50 |
51 | //************************
52 | // WHEN
53 | //************************
54 | AtomicBoolean firstTryToGetLock = new AtomicBoolean(false);
55 | boolean gotLockSuccessfully = redUtilsLock.tryAcquire("lock", () -> {
56 | firstTryToGetLock.set(true);
57 | });
58 |
59 |
60 | //************************
61 | // THEN
62 | //************************
63 | Assertions.assertTrue(firstTryToGetLock.get());
64 | Assertions.assertTrue(gotLockSuccessfully);
65 | Assertions.assertNull(getKey("lock"));
66 | }
67 |
68 | @Test
69 | void test_tryAcquire_WHEN_redis_stop_after_getting_lock_THEN_we_SHOULD_NOT_able_to_get_new_lock() throws Exception {
70 | //************************
71 | // Given
72 | //************************
73 | RedisServer redisServer = new RedisServer();
74 | RedisAddress redisAddress = redisServer.startSingleInstance();
75 |
76 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(redisAddress.masterHostAddress, redisAddress.masterPort);
77 |
78 | AtomicBoolean firstThreadTryToGetLock = new AtomicBoolean(false);
79 |
80 | //************************
81 | // WHEN
82 | //************************
83 | Thread firstThread = new Thread(
84 | new Runnable() {
85 | @Override
86 | public void run() {
87 | redUtilsLock.tryAcquire("lock", () -> {
88 | firstThreadTryToGetLock.set(true);
89 | sleepSeconds(5);
90 | });
91 | }
92 | });
93 |
94 | firstThread.start();
95 | Awaitility.await("check first thread can get the lock").untilTrue(firstThreadTryToGetLock);
96 |
97 | redisServer.shutDown();
98 |
99 | //************************
100 | // THEN
101 | //************************
102 | Assertions.assertThrows(JedisConnectionException.class, () -> {
103 | redUtilsLock.tryAcquire("lock", () -> {
104 | sleepSeconds(5);
105 | });
106 | });
107 | }
108 |
109 | @Test
110 | void test_tryAcquire_WHEN_redis_become_unavailable_after_getting_lock_THEN_we_SHOULD_get_exception() throws Exception {
111 | //************************
112 | // Given
113 | //************************
114 | RedisServer redisServer = new RedisServer();
115 | RedisAddress redisAddress = redisServer.startSingleInstance();
116 | RedUtilsConfig redUtilsConfig = new RedUtilsConfig.RedUtilsConfigBuilder()
117 | .hostAddress(redisAddress.masterHostAddress)
118 | .port(redisAddress.masterPort)
119 | .leaseTimeMillis(10_000)
120 | .build();
121 |
122 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(redUtilsConfig);
123 | AtomicBoolean firstThreadTryToGetLock = new AtomicBoolean(false);
124 | AtomicReference raisedException = new AtomicReference<>();
125 |
126 | //************************
127 | // WHEN
128 | //************************
129 | CompletableFuture runningFirstThreadFuture = CompletableFuture.runAsync(() -> {
130 | redUtilsLock.tryAcquire("lock1", () -> {
131 | firstThreadTryToGetLock.set(true);
132 | sleepSeconds(60);
133 | });
134 | });
135 |
136 | runningFirstThreadFuture.exceptionally(throwable -> {
137 | raisedException.set(throwable.getCause());
138 | return null;
139 | });
140 |
141 | Awaitility.await("check first thread can get the lock").forever().untilTrue(firstThreadTryToGetLock);
142 |
143 | redisServer.pauseMaster(60);
144 |
145 | //************************
146 | // THEN
147 | //************************
148 | try {
149 | runningFirstThreadFuture.join();
150 | } catch (Exception exception) {
151 | Awaitility.await("check raised exception is set").until(() -> {
152 | return raisedException.get() != null;
153 | });
154 | }
155 |
156 | Assertions.assertTrue(runningFirstThreadFuture.isCompletedExceptionally());
157 | Assertions.assertTrue(raisedException.get() instanceof RefreshLockException);
158 | }
159 |
160 | @Test
161 | void test_tryAcquire_WHEN_two_threads_want_to_get_lock_THEN_last_one_SHOULD_NOT_be_able_to_get_lock() throws Exception {
162 | //************************
163 | // Given
164 | //************************
165 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(GENERAL_REDIS_ADDRESS.masterHostAddress, GENERAL_REDIS_ADDRESS.masterPort);
166 |
167 | AtomicBoolean firstThreadTryToGetLock = new AtomicBoolean(false);
168 | AtomicBoolean secondThreadTryToGetLock = new AtomicBoolean(false);
169 |
170 | AtomicBoolean firstThreadGotLockSuccessfully = new AtomicBoolean(false);
171 | AtomicBoolean secondThreadGotLockSuccessfully = new AtomicBoolean(false);
172 |
173 | //************************
174 | // WHEN
175 | //************************
176 |
177 | Thread firstThread = new Thread(
178 | new Runnable() {
179 | @Override
180 | public void run() {
181 | boolean isSuccess = redUtilsLock.tryAcquire("lock", () -> {
182 | firstThreadTryToGetLock.set(true);
183 | sleepSeconds(3);
184 | });
185 |
186 | firstThreadGotLockSuccessfully.set(isSuccess);
187 |
188 | }
189 | });
190 |
191 | firstThread.start();
192 |
193 | Awaitility.await("check first thread can get the lock").untilTrue(firstThreadTryToGetLock);
194 |
195 | Thread secondThread = new Thread(
196 | new Runnable() {
197 | @Override
198 | public void run() {
199 | boolean isSuccess = redUtilsLock.tryAcquire("lock", () -> {
200 | secondThreadTryToGetLock.set(true);
201 | });
202 |
203 | secondThreadGotLockSuccessfully.set(isSuccess);
204 | }
205 | });
206 |
207 | secondThread.start();
208 |
209 | firstThread.join();
210 | secondThread.join();
211 |
212 | //************************
213 | // THEN
214 | //************************
215 | Assertions.assertTrue(firstThreadTryToGetLock.get());
216 | Assertions.assertTrue(firstThreadGotLockSuccessfully.get());
217 | Assertions.assertFalse(secondThreadTryToGetLock.get());
218 | Assertions.assertFalse(secondThreadGotLockSuccessfully.get());
219 | }
220 |
221 | @Test
222 | void test_acquire_WHEN_two_threads_want_to_get_lock_THEN_last_one_SHOULD_NOT_be_able_to_get_lock() throws Exception {
223 | //************************
224 | // Given
225 | //************************
226 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(GENERAL_REDIS_ADDRESS.masterHostAddress, GENERAL_REDIS_ADDRESS.masterPort);
227 |
228 | AtomicBoolean firstTryToGetLock = new AtomicBoolean(false);
229 | AtomicBoolean secondTryToGetLock = new AtomicBoolean(false);
230 |
231 |
232 | //************************
233 | // WHEN
234 | //************************
235 | Thread firstThread = new Thread(
236 | new Runnable() {
237 | @Override
238 | public void run() {
239 | redUtilsLock.acquire("lock1", () -> {
240 | firstTryToGetLock.set(true);
241 | sleepSeconds(12);
242 | });
243 | }
244 | });
245 |
246 | firstThread.start();
247 |
248 | Awaitility.await("check first thread can get the lock").untilTrue(firstTryToGetLock);
249 |
250 |
251 | Thread secondThread = new Thread(
252 | new Runnable() {
253 | @Override
254 | public void run() {
255 | redUtilsLock.acquire("lock1", () -> {
256 | secondTryToGetLock.set(true);
257 | });
258 | }
259 | });
260 |
261 | secondThread.start();
262 |
263 | Awaitility.await("second thread should not get lock").pollDelay(Duration.ofSeconds(2))
264 | .atMost(Duration.ofSeconds(3))
265 | .untilFalse(secondTryToGetLock);
266 |
267 |
268 | //************************
269 | // THEN
270 | //************************
271 | Assertions.assertTrue(firstTryToGetLock.get());
272 | Assertions.assertFalse(secondTryToGetLock.get());
273 |
274 | }
275 |
276 | @Disabled("Disabled until implement reentrancy")
277 | @Test()
278 | void test_get_lock_WHEN_reentrancy_in_the_same_thread_we_SHOULD_able_to_proceed() throws Exception {
279 | //************************
280 | // Given
281 | //************************
282 | RedisServer redisServer = new RedisServer();
283 | RedisAddress redisAddress = redisServer.startSingleInstance();
284 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(redisAddress.masterHostAddress, redisAddress.masterPort);
285 |
286 | AtomicBoolean firstLock = new AtomicBoolean(false);
287 | AtomicBoolean secondLock = new AtomicBoolean(false);
288 |
289 | //************************
290 | // WHEN
291 | //************************
292 | Thread firstThread = new Thread(
293 | new Runnable() {
294 | @Override
295 | public void run() {
296 | redUtilsLock.acquire("lock1", () -> {
297 | firstLock.set(true);
298 |
299 | redUtilsLock.acquire("lock1", () -> {
300 | secondLock.set(true);
301 | });
302 | });
303 | }
304 | });
305 |
306 | firstThread.start();
307 |
308 |
309 | Awaitility.await("check thread can get the first lock").atMost(Duration.ofMinutes(10)).untilTrue(firstLock);
310 | Awaitility.await("check thread can get the second lock").atMost(Duration.ofMinutes(10)).untilTrue(secondLock);
311 |
312 |
313 | //************************
314 | // THEN
315 | //************************
316 | Assertions.assertTrue(firstLock.get());
317 | Assertions.assertFalse(secondLock.get());
318 | }
319 |
320 | @Test
321 | void test_acquire_WHEN_happy_path() throws Exception {
322 | //************************
323 | // Given
324 | //************************
325 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(GENERAL_REDIS_ADDRESS.masterHostAddress, GENERAL_REDIS_ADDRESS.masterPort);
326 | AtomicBoolean firstTryToGetLock = new AtomicBoolean(false);
327 |
328 |
329 | //************************
330 | // WHEN
331 | //************************
332 | redUtilsLock.acquire("lock1", () -> {
333 | sleepSeconds(2);
334 | firstTryToGetLock.set(true);
335 | });
336 |
337 |
338 | //************************
339 | // THEN
340 | //************************
341 | Assertions.assertTrue(firstTryToGetLock.get());
342 | Assertions.assertNull(getKey("lock1"));
343 |
344 | }
345 |
346 | @Test
347 | void test_acquire_get_lock_multiple_time() throws Exception {
348 | //************************
349 | // Given
350 | //************************
351 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(GENERAL_REDIS_ADDRESS.masterHostAddress, GENERAL_REDIS_ADDRESS.masterPort);
352 | AtomicBoolean tryToGetLock = new AtomicBoolean(false);
353 |
354 | //************************
355 | // WHEN
356 | //************************
357 | redUtilsLock.acquire("lock1", () -> {
358 | sleepSeconds(1);
359 | tryToGetLock.set(true);
360 | });
361 |
362 | tryToGetLock.set(false);
363 |
364 | redUtilsLock.acquire("lock1", () -> {
365 | sleepSeconds(1);
366 | tryToGetLock.set(true);
367 | });
368 |
369 | //************************
370 | // THEN
371 | //************************
372 | Assertions.assertTrue(tryToGetLock.get());
373 | Assertions.assertNull(getKey("lock1"));
374 |
375 | }
376 |
377 | @Test
378 | void test_acquire_WHEN_single_client_AND_multiple_threads_process_the_same_resource_THEN_we_SHOULD_get_correct_result() throws Exception {
379 | //************************
380 | // Given
381 | //************************
382 | final int threadCount = 30;
383 | final int expectedResourceValue = threadCount;
384 | final AtomicInteger sharedResource = new AtomicInteger(0);
385 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(GENERAL_REDIS_ADDRESS.masterHostAddress, GENERAL_REDIS_ADDRESS.masterPort);
386 | ExecutorService executorService = Executors.newCachedThreadPool();
387 |
388 | //************************
389 | // WHEN
390 | //************************
391 | for (int i = 0; i < threadCount; i++) {
392 | executorService.submit(() -> {
393 | redUtilsLock.acquire("lock1", () -> {
394 | int resValue = sharedResource.get();
395 | resValue = resValue + 1;
396 | sharedResource.set(resValue);
397 | });
398 | });
399 | }
400 | executorService.shutdown();
401 | boolean allThreadExecutionFinished = executorService.awaitTermination(1, TimeUnit.MINUTES);
402 |
403 | //************************
404 | // THEN
405 | //************************
406 |
407 | Assertions.assertTrue(allThreadExecutionFinished);
408 | Assertions.assertEquals(expectedResourceValue, sharedResource.get());
409 | }
410 |
411 | @Test
412 | void test_acquire_WHEN_multiple_client_AND_multiple_threads_process_the_same_resource_THEN_we_SHOULD_get_correct_result() throws Exception {
413 | //************************
414 | // Given
415 | //************************
416 | final int threadCountPerClient = 10;
417 | final int clientCount = 5;
418 | final int expectedResourceValue = threadCountPerClient * clientCount;
419 | final AtomicInteger sharedResource = new AtomicInteger(0);
420 | final List clients = new ArrayList<>();
421 |
422 | for (int i = 0; i < clientCount; i++) {
423 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(GENERAL_REDIS_ADDRESS.masterHostAddress, GENERAL_REDIS_ADDRESS.masterPort);
424 | clients.add(redUtilsLock);
425 | }
426 |
427 | ExecutorService executorService = Executors.newCachedThreadPool();
428 |
429 | //************************
430 | // WHEN
431 | //************************
432 | for (int i = 0; i < clientCount; i++) {
433 | for (int j = 0; j < threadCountPerClient; j++) {
434 | final int clientId = i;
435 | executorService.submit(() -> {
436 | clients.get(clientId).acquire("lock1", () -> {
437 | int resValue = sharedResource.get();
438 | resValue = resValue + 1;
439 | sharedResource.set(resValue);
440 | });
441 | });
442 | }
443 | }
444 |
445 | executorService.shutdown();
446 | boolean allThreadExecutionFinished = executorService.awaitTermination(1, TimeUnit.MINUTES);
447 |
448 | //************************
449 | // THEN
450 | //************************
451 | Assertions.assertTrue(allThreadExecutionFinished);
452 | Assertions.assertEquals(expectedResourceValue, sharedResource.get());
453 |
454 | }
455 |
456 | @Test
457 | void test_acquire_WHEN_one_of_replicas_is_unavailable_THEN_we_SHOULD_get_exception() throws Exception {
458 | //************************
459 | // Given
460 | //************************
461 | final int replicaCount = 3;
462 | RedisServer redisServer = new RedisServer();
463 | RedisAddress redisAddress = redisServer.startMasterReplicas(replicaCount);
464 | RedUtilsConfig redUtilsConfig = new RedUtilsConfig.RedUtilsConfigBuilder()
465 | .hostAddress(redisAddress.masterHostAddress)
466 | .port(redisAddress.masterPort)
467 | .replicaCount(replicaCount)
468 | .leaseTimeMillis(10_000)
469 | .build();
470 |
471 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(redUtilsConfig);
472 |
473 |
474 | //************************
475 | // WHEN
476 | //************************
477 | redisServer.shutdownReplica(1);
478 | //************************
479 | // THEN
480 | //************************
481 | Assertions.assertThrows(ReplicaIsDownException.class, () -> {
482 | redUtilsLock.acquire("lock1", () -> {
483 | sleepSeconds(10);
484 | });
485 | });
486 |
487 |
488 | }
489 |
490 | @Test
491 | void test_acquire_WHEN_one_of_replicas_become_unavailable_after_getting_lock_THEN_we_SHOULD_get_exception() throws Exception {
492 | //************************
493 | // Given
494 | //************************
495 | final int replicaCount = 3;
496 | RedisServer redisServer = new RedisServer();
497 | RedisAddress redisAddress = redisServer.startMasterReplicas(replicaCount);
498 | RedUtilsConfig redUtilsConfig = new RedUtilsConfig.RedUtilsConfigBuilder()
499 | .hostAddress(redisAddress.masterHostAddress)
500 | .port(redisAddress.masterPort)
501 | .replicaCount(replicaCount)
502 | .leaseTimeMillis(10_000)
503 | .build();
504 |
505 | RedUtilsLockImpl redUtilsLock = new RedUtilsLockImpl(redUtilsConfig);
506 |
507 | AtomicBoolean firstThreadTryToGetLock = new AtomicBoolean(false);
508 | AtomicReference raisedException = new AtomicReference<>();
509 |
510 | //************************
511 | // WHEN
512 | //************************
513 | CompletableFuture runningFirstThreadFuture = CompletableFuture.runAsync(() -> {
514 | redUtilsLock.acquire("lock1", () -> {
515 | firstThreadTryToGetLock.set(true);
516 | sleepSeconds(60);
517 | });
518 | });
519 |
520 | runningFirstThreadFuture.exceptionally(throwable -> {
521 | raisedException.set(throwable.getCause());
522 | return null;
523 | });
524 |
525 | Awaitility.await("check first thread can get the lock").untilTrue(firstThreadTryToGetLock);
526 |
527 | redisServer.pauseReplica(1, 60);
528 |
529 |
530 | //************************
531 | // THEN
532 | //************************
533 | try {
534 | runningFirstThreadFuture.join();
535 | } catch (Exception exception) {
536 | Awaitility.await("check raisedException is set").until(() -> {
537 | return raisedException.get() != null;
538 | });
539 | }
540 |
541 | Assertions.assertTrue(runningFirstThreadFuture.isCompletedExceptionally());
542 | Assertions.assertTrue(raisedException.get() instanceof RefreshLockException);
543 | }
544 |
545 |
546 | private String getKey(String key) {
547 | return JEDIS.get(key);
548 | }
549 |
550 | }
--------------------------------------------------------------------------------
/src/test/java/org/github/siahsang/redutils/common/connection/JedisConnectionManagerTest.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.redutils.common.connection;
2 |
3 | import org.github.siahsang.redutils.AbstractBaseTest;
4 | import org.github.siahsang.redutils.common.RedUtilsConfig;
5 | import org.github.siahsang.redutils.exception.BadRequestException;
6 | import org.github.siahsang.redutils.exception.InsufficientResourceException;
7 | import org.github.siahsang.test.redis.RedisAddress;
8 | import org.github.siahsang.test.redis.RedisServer;
9 | import org.junit.jupiter.api.Assertions;
10 | import org.junit.jupiter.api.BeforeEach;
11 | import org.junit.jupiter.api.Test;
12 | import org.testcontainers.junit.jupiter.Testcontainers;
13 | import redis.clients.jedis.Jedis;
14 |
15 | /**
16 | * @author Javad Alimohammadi
17 | */
18 |
19 | @Testcontainers
20 | class JedisConnectionManagerTest extends AbstractBaseTest {
21 |
22 | private final static RedisAddress GENERAL_REDIS_ADDRESS = new RedisServer().startSingleInstance();
23 |
24 | private final static int INITIAL_CAPACITY = 10;
25 |
26 | private JedisConnectionManager jedisConnectionManager;
27 |
28 | @BeforeEach
29 | public void init() {
30 | RedUtilsConfig redUtilsConfig = new RedUtilsConfig.
31 | RedUtilsConfigBuilder()
32 | .hostAddress(GENERAL_REDIS_ADDRESS.masterHostAddress)
33 | .port(GENERAL_REDIS_ADDRESS.masterPort)
34 | .maxPoolSize(INITIAL_CAPACITY)
35 | .build();
36 |
37 | jedisConnectionManager = new JedisConnectionManager(redUtilsConfig);
38 |
39 | }
40 |
41 | @Test
42 | public void WHEN_get_connection_THEN_happy_path() throws Exception {
43 | //************************
44 | // Given
45 | //************************
46 |
47 | //************************
48 | // WHEN - THEN
49 | //************************
50 | boolean reserveFiveConnection = jedisConnectionManager.reserve(7);
51 | Assertions.assertTrue(reserveFiveConnection);
52 |
53 |
54 | Jedis jedisConnection1 = jedisConnectionManager.borrow();
55 | Jedis jedisConnection2 = jedisConnectionManager.borrow();
56 | Jedis jedisConnection3 = jedisConnectionManager.borrow();
57 | Jedis jedisConnection4 = jedisConnectionManager.borrow();
58 | Jedis jedisConnection5 = jedisConnectionManager.borrow();
59 | Jedis jedisConnection6 = jedisConnectionManager.borrow();
60 | Jedis jedisConnection7 = jedisConnectionManager.borrow();
61 |
62 | Assertions.assertEquals(3, jedisConnectionManager.remainingCapacity());
63 |
64 |
65 | jedisConnectionManager.returnBack(jedisConnection1);
66 | jedisConnectionManager.returnBack(jedisConnection2);
67 | jedisConnectionManager.returnBack(jedisConnection3);
68 | jedisConnectionManager.returnBack(jedisConnection4);
69 | jedisConnectionManager.returnBack(jedisConnection5);
70 | jedisConnectionManager.returnBack(jedisConnection6);
71 | jedisConnectionManager.returnBack(jedisConnection7);
72 |
73 | Assertions.assertEquals(INITIAL_CAPACITY, jedisConnectionManager.remainingCapacity());
74 | }
75 |
76 |
77 | @Test
78 | public void WHEN_get_more_connection_than_capacity_THEN_false_SHOULD_be_returned() throws Exception {
79 | //************************
80 | // Given
81 | //************************
82 |
83 | //************************
84 | // WHEN
85 | //************************
86 | boolean reservedSuccessfully = jedisConnectionManager.reserve(INITIAL_CAPACITY + 1);
87 |
88 | //************************
89 | // THEN
90 | //************************
91 | Assertions.assertFalse(reservedSuccessfully);
92 | Assertions.assertEquals(INITIAL_CAPACITY, jedisConnectionManager.remainingCapacity());
93 | }
94 |
95 |
96 | @Test
97 | public void WHEN_brow_connection_more_than_reserved_THEN_exception_SHOULD_be_thrown() throws Exception {
98 | //************************
99 | // Given
100 | //************************
101 |
102 | //************************
103 | // WHEN
104 | //************************
105 | jedisConnectionManager.reserve(3);
106 |
107 | //************************
108 | // THEN
109 | //************************
110 | jedisConnectionManager.borrow();
111 | jedisConnectionManager.borrow();
112 | jedisConnectionManager.borrow();
113 |
114 | Assertions.assertThrows(InsufficientResourceException.class, () -> {
115 | jedisConnectionManager.borrow();
116 | });
117 |
118 | }
119 |
120 |
121 | @Test
122 | public void WHEN_get_connection_with_invalid_name_THEN_exception_SHOULD_be_thrown() throws Exception {
123 | //************************
124 | // Given
125 | //************************
126 |
127 |
128 | //************************
129 | // WHEN
130 | //************************
131 | jedisConnectionManager.reserve(3);
132 |
133 | //************************
134 | // THEN
135 | //************************
136 | Assertions.assertThrows(BadRequestException.class, () -> {
137 | jedisConnectionManager.borrow("invalid_resource_id");
138 | });
139 |
140 |
141 | }
142 |
143 | @Test
144 | public void WHEN_get_connection_with_multiple_thread_THEN_capacity_SHOULD_be_set_accordingly() throws Exception {
145 | //************************
146 | // Given
147 | //************************
148 |
149 | //************************
150 | // WHEN
151 | //************************
152 | Thread firstThread = new Thread(
153 | new Runnable() {
154 | @Override
155 | public void run() {
156 | jedisConnectionManager.reserve(3);
157 | }
158 | });
159 |
160 | Thread secondThread = new Thread(
161 | new Runnable() {
162 | @Override
163 | public void run() {
164 | jedisConnectionManager.reserve(3);
165 | }
166 | });
167 |
168 | Thread thirdThread = new Thread(
169 | new Runnable() {
170 | @Override
171 | public void run() {
172 | jedisConnectionManager.reserve(3);
173 | }
174 | });
175 |
176 | firstThread.start();
177 | secondThread.start();
178 | thirdThread.start();
179 |
180 | firstThread.join();
181 | secondThread.join();
182 | thirdThread.join();
183 |
184 | //************************
185 | // THEN
186 | //************************
187 |
188 | Assertions.assertEquals(1, jedisConnectionManager.remainingCapacity());
189 |
190 | }
191 |
192 | }
--------------------------------------------------------------------------------
/src/test/java/org/github/siahsang/test/redis/AOFConfiguration.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.test.redis;
2 |
3 | public enum AOFConfiguration {
4 | ALWAYS("always"), EVERY_SECOND("everysec"), NO("no");
5 |
6 | public String value;
7 |
8 | AOFConfiguration(String value) {
9 | this.value = value;
10 | }
11 |
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/java/org/github/siahsang/test/redis/RedisAddress.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.test.redis;
2 |
3 | public class RedisAddress {
4 | public final int masterPort;
5 |
6 | public final String masterHostAddress;
7 |
8 | public RedisAddress(int masterPort, String masterHostAddress) {
9 | this.masterPort = masterPort;
10 | this.masterHostAddress = masterHostAddress;
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/java/org/github/siahsang/test/redis/RedisServer.java:
--------------------------------------------------------------------------------
1 | package org.github.siahsang.test.redis;
2 |
3 | import org.github.siahsang.redutils.RedUtilsLockImpl;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.testcontainers.containers.GenericContainer;
7 | import org.testcontainers.images.builder.ImageFromDockerfile;
8 |
9 | import java.io.IOException;
10 | import java.nio.file.Path;
11 | import java.nio.file.Paths;
12 | import java.util.List;
13 |
14 |
15 | public class RedisServer {
16 | private static final Logger log = LoggerFactory.getLogger(RedUtilsLockImpl.class);
17 |
18 | private final Path RESOURCE_PATH = Paths.get("src/test/resources");
19 |
20 | public ImageFromDockerfile buildImageDockerfile() {
21 | return new ImageFromDockerfile("redutils-redis-image_tmp", false)
22 | .withFileFromPath(".", RESOURCE_PATH);
23 | }
24 |
25 | public final static int DEFAULT_CONTAINER_MASTER_PORT = 6379;
26 |
27 | private GenericContainer> redisContainer;
28 |
29 | public RedisAddress startSingleInstance() {
30 | return startRedis(0, AOFConfiguration.ALWAYS);
31 | }
32 |
33 | public RedisAddress startMasterReplicas(final int replicaCount) {
34 | return startRedis(replicaCount, AOFConfiguration.ALWAYS);
35 | }
36 |
37 |
38 | public RedisAddress startRedis(final int replicaCount, final AOFConfiguration AOFConfig) {
39 | String enableAOFStr = "no";
40 |
41 | if (!AOFConfig.equals(AOFConfiguration.NO)) {
42 | enableAOFStr = "yes";
43 | }
44 |
45 | Integer[] exposedPorts = new Integer[1 + replicaCount];
46 |
47 | exposedPorts[0] = DEFAULT_CONTAINER_MASTER_PORT;
48 |
49 | for (int i = 1; i <= replicaCount; i++) {
50 | exposedPorts[i] = DEFAULT_CONTAINER_MASTER_PORT + i;
51 | }
52 |
53 | redisContainer = new GenericContainer(buildImageDockerfile())
54 | .withEnv("MASTER_PORT", String.valueOf(DEFAULT_CONTAINER_MASTER_PORT))
55 | .withEnv("REPLICAS", String.valueOf(replicaCount))
56 | .withEnv("AOF", enableAOFStr)
57 | .withEnv("AOF_CONFIG", AOFConfig.value)
58 | .withExposedPorts(exposedPorts);
59 |
60 | redisContainer.start();
61 |
62 | return new RedisAddress(redisContainer.getFirstMappedPort(), redisContainer.getHost());
63 | }
64 |
65 | public void shutDown() {
66 | redisContainer.stop();
67 | }
68 |
69 | public void shutdownMaster() throws IOException, InterruptedException {
70 | redisContainer.execInContainer("redis-cli", "-p", String.valueOf(DEFAULT_CONTAINER_MASTER_PORT), "SHUTDOWN");
71 | }
72 |
73 | public void shutdownReplica(final int replicaNumber) throws IOException, InterruptedException {
74 | List exposedPorts = redisContainer.getExposedPorts();
75 | raiseExceptionIfNoReplicaAvailable();
76 |
77 | final int port = replicaNumber + 1; // because first port is for master
78 | log.info("Shutting Down replica no [{}]", replicaNumber);
79 | redisContainer.execInContainer("redis-cli", "-p", String.valueOf(exposedPorts.get(port)), "SHUTDOWN");
80 | }
81 |
82 | public void pauseMaster(final int seconds) throws IOException, InterruptedException {
83 | List exposedPorts = redisContainer.getExposedPorts();
84 | final String pauseTimeInMillis = String.valueOf(seconds * 1000);
85 | log.info("Pausing Master for [{}] second(s)", seconds);
86 | redisContainer.execInContainer("redis-cli", "-p",
87 | String.valueOf(exposedPorts.get(0)), "CLIENT", "PAUSE", pauseTimeInMillis);
88 | }
89 |
90 |
91 | public void pauseReplica(final int replicaNumber, final int seconds) throws IOException, InterruptedException {
92 | List exposedPorts = redisContainer.getExposedPorts();
93 | raiseExceptionIfNoReplicaAvailable();
94 |
95 | final int port = replicaNumber + 1; // because first port is for master
96 | final String pauseTimeInMillis = String.valueOf(seconds * 1000);
97 | log.info("Pausing replica number [{}] for [{}] second(s)", replicaNumber, seconds);
98 | redisContainer.execInContainer("redis-cli", "-p",
99 | String.valueOf(exposedPorts.get(port)), "CLIENT", "PAUSE", pauseTimeInMillis);
100 | }
101 |
102 |
103 | private void raiseExceptionIfNoReplicaAvailable() {
104 | List exposedPorts = redisContainer.getExposedPorts();
105 | if (exposedPorts.size() <= 1) {
106 | throw new IllegalArgumentException("There are no any replica");
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/test/resources/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.7
2 |
3 | LABEL maintainer="Javad Alimohammadi "
4 |
5 | # Install system dependencies
6 | RUN apk update
7 | RUN apk add bash
8 | RUN apk add wget
9 | RUN apk add make
10 | RUN apk add gcc
11 | RUN apk add musl-dev
12 | RUN apk add linux-headers
13 | RUN apk add tcl
14 |
15 |
16 | # Get the last stable version of redis
17 | WORKDIR /home
18 | RUN wget -O redis.tar.gz http://download.redis.io/redis-stable.tar.gz
19 | RUN tar xfz redis.tar.gz
20 | RUN rm redis.tar.gz
21 | RUN mv redis-* redis
22 | WORKDIR /home/redis
23 | RUN make
24 | RUN cp /home/redis/src/redis-server /usr/local/bin/
25 | RUN cp /home/redis/src/redis-cli /usr/local/bin/
26 |
27 | COPY docker-entrypoint.sh /docker-entrypoint.sh
28 | RUN chmod 777 /docker-entrypoint.sh
29 |
30 | EXPOSE 6379
31 | ENTRYPOINT ["/docker-entrypoint.sh", "--protected-mode no"]
32 |
--------------------------------------------------------------------------------
/src/test/resources/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | function wait_for_redis_become_available() {
4 | local redis_name=$1
5 | local port=$2
6 | local is_started_fully=$(redis-cli -p "$port" PING | grep PONG)
7 | echo "Waiting for Redis $redis_name to become available"
8 | while [ -z "$is_started_fully" ]; do
9 | sleep 1
10 | is_started_fully=$(redis-cli -p "$MASTER_PORT" PING | grep PONG)
11 | done
12 | echo "$1 Redis started fully at port $MASTER_PORT"
13 | }
14 |
15 | readonly MASTER_PORT="${MASTER_PORT:-6379}"
16 | readonly REPLICAS="${REPLICAS:-3}"
17 | readonly AOF="${AOF:-no}"
18 | readonly AOF_CONFIG="${AOF_CONFIG:-everysec}"
19 |
20 | # make redis-master home directory
21 | readonly MASTER_DIR=/etc/redis/redis-master
22 | readonly REDIS_CONFIG_FILE=redis.conf
23 |
24 | mkdir -p "$MASTER_DIR"
25 | cd "$MASTER_DIR"
26 |
27 |
28 | # create config file for the master
29 | touch "$REDIS_CONFIG_FILE"
30 | echo "protected-mode no" >>"$REDIS_CONFIG_FILE"
31 | echo "dir $MASTER_DIR" >>"$REDIS_CONFIG_FILE"
32 | echo "port $MASTER_PORT" >>"$REDIS_CONFIG_FILE"
33 | echo "appendonly $AOF" >>"$REDIS_CONFIG_FILE"
34 | echo "appendfsync $AOF_CONFIG" >>"$REDIS_CONFIG_FILE"
35 |
36 | # start the master
37 |
38 | redis-server "$MASTER_DIR/$REDIS_CONFIG_FILE" &
39 | wait_for_redis_become_available "MASTER" "$MASTER_PORT"
40 |
41 |
42 |
43 | # starting replicas
44 | for ((i = 1; i <= REPLICAS; i++)); do
45 | replica_dir="/etc/redis/redis-replica-${i}"
46 | mkdir -p "$replica_dir"
47 | cd "$replica_dir"
48 | touch "$REDIS_CONFIG_FILE"
49 | echo "dir $replica_dir" >>"$REDIS_CONFIG_FILE"
50 | echo "port $((MASTER_PORT + i)) " >>"$REDIS_CONFIG_FILE"
51 | echo "appendonly $AOF" >>"$REDIS_CONFIG_FILE"
52 | echo "appendfsync $AOF_CONFIG" >>"$REDIS_CONFIG_FILE"
53 | echo "replicaof 127.0.0.1 $MASTER_PORT" >>"$REDIS_CONFIG_FILE"
54 | redis-server "$replica_dir/$REDIS_CONFIG_FILE" &
55 |
56 | done
57 |
58 | for ((i = 1; i <= REPLICAS; i++)); do
59 | wait_for_redis_become_available "REPLICA $i" "$((MASTER_PORT + i))"
60 | done
61 |
62 |
63 | tail -f /dev/null
64 |
--------------------------------------------------------------------------------
/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%logger{35}){}\(%line\) -- %msg%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/test/resources/simplelogger.properties:
--------------------------------------------------------------------------------
1 | # SLF4J's SimpleLogger configuration file
2 | # Simple implementation of Logger that sends all enabled log messages
3 |
4 | # Default logging detail level for all instances of SimpleLogger.
5 | # Must be one of ("trace", "debug", "info", "warn", or "error").
6 | # If not specified, defaults to "info".
7 | org.slf4j.simpleLogger.defaultLogLevel=info
8 |
9 | # Logging detail level for a SimpleLogger instance named "xxxxx".
10 | # Must be one of ("trace", "debug", "info", "warn", or "error").
11 | # If not specified, the default logging detail level is used.
12 | org.slf4j.simpleLogger.log.org.github.siahsang=trace
13 |
14 | # Set to true if you want the current date and time to be included in output messages.
15 | # Default is false, and will output the number of milliseconds elapsed since startup.
16 | org.slf4j.simpleLogger.showDateTime=true
17 |
18 | # The date and time format to be used in the output messages.
19 | # The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat.
20 | # If the format is not specified or is invalid, the default format is used.
21 | # The default format is yyyy-MM-dd HH:mm:ss:SSS Z.
22 | org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS
23 |
24 | # Set to true if you want to output the current thread name.
25 | # Defaults to true.
26 | #org.slf4j.simpleLogger.showThreadName=true
27 |
28 | # Set to true if you want the Logger instance name to be included in output messages.
29 | # Defaults to true.
30 | #org.slf4j.simpleLogger.showLogName=true
31 |
32 | # Set to true if you want the last component of the name to be included in output messages.
33 | # Defaults to false.
34 | #org.slf4j.simpleLogger.showShortLogName=false
35 |
36 | # The output target which can be the path to a file, or the special values "System.out" and
37 | # System.err". Default is "System.err".
38 |
39 |
40 | org.slf4j.simpleLogger.logFile = System.out
--------------------------------------------------------------------------------