Skip to content

Commit 5e6ae90

Browse files
committed
spring-projectsGH-3444: Custom TTL per LOCK in RedisLockRegistry and JdbcLockRegistry
1 parent c155d5d commit 5e6ae90

File tree

4 files changed

+178
-19
lines changed

4 files changed

+178
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.support.locks;
18+
19+
import java.util.concurrent.TimeUnit;
20+
import java.util.concurrent.locks.Lock;
21+
22+
/**
23+
* A {@link Lock} implementing this interface supports the spring distributed locks with custom time-to-live value per lock
24+
*
25+
* @author Eddie Cho
26+
*
27+
* @since 6.3
28+
*/
29+
public interface CustomTtlLock extends Lock {
30+
31+
/**
32+
* Attempt to acquire a lock with a specific time-to-live
33+
* @param time the maximum time to wait for the lock unit
34+
* @param unit the time unit of the time argument
35+
* @param customTtl the specific time-to-live for the lock status data
36+
* @param customTtlUnit the time unit of the customTtl argument
37+
* @return true if the lock was acquired and false if the waiting time elapsed before the lock was acquired
38+
* @throws InterruptedException -
39+
* if the current thread is interrupted while acquiring the lock (and interruption of lock acquisition is supported)
40+
*/
41+
boolean tryLock(long time, TimeUnit unit, long customTtl, TimeUnit customTtlUnit) throws InterruptedException;
42+
43+
/**
44+
* Attempt to acquire a lock with a specific time-to-live
45+
* @param customTtl the specific time-to-live for the lock status data
46+
* @param customTtlUnit the time unit of the customTtl argument
47+
*/
48+
void lock(long customTtl, TimeUnit customTtlUnit);
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* A {@link LockRegistry} implementing this interface supports the CustomTtlLock
19+
*
20+
* @author Eddie Cho
21+
*
22+
* @since 6.3
23+
*/
24+
package org.springframework.integration.support.locks;
25+
26+
public interface CustomTtlLockRegistry extends LockRegistry {
27+
28+
CustomTtlLock obtainCustomTtlLock(Object lockKey);
29+
}

Diff for: spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java

+39-19
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
import org.springframework.data.redis.listener.ChannelTopic;
5252
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
5353
import org.springframework.data.redis.listener.Topic;
54+
import org.springframework.integration.support.locks.CustomTtlLock;
55+
import org.springframework.integration.support.locks.CustomTtlLockRegistry;
5456
import org.springframework.integration.support.locks.ExpirableLockRegistry;
5557
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
5658
import org.springframework.util.Assert;
@@ -90,7 +92,7 @@
9092
* @since 4.0
9193
*
9294
*/
93-
public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean {
95+
public final class RedisLockRegistry implements ExpirableLockRegistry, CustomTtlLockRegistry, DisposableBean {
9496

9597
private static final Log LOGGER = LogFactory.getLog(RedisLockRegistry.class);
9698

@@ -225,6 +227,11 @@ public void setRedisLockType(RedisLockType redisLockType) {
225227

226228
@Override
227229
public Lock obtain(Object lockKey) {
230+
return this.obtainCustomTtlLock(lockKey);
231+
}
232+
233+
@Override
234+
public CustomTtlLock obtainCustomTtlLock(Object lockKey) {
228235
Assert.isInstanceOf(String.class, lockKey);
229236
String path = (String) lockKey;
230237
this.lock.lock();
@@ -296,7 +303,7 @@ private Function<String, RedisLock> getRedisLockConstructor(RedisLockType redisL
296303
};
297304
}
298305

299-
private abstract class RedisLock implements Lock {
306+
private abstract class RedisLock implements CustomTtlLock {
300307

301308
private static final String OBTAIN_LOCK_SCRIPT = """
302309
local lockClientId = redis.call('GET', KEYS[1])
@@ -334,11 +341,12 @@ public long getLockedAt() {
334341
/**
335342
* Attempt to acquire a lock in redis.
336343
* @param time the maximum time(milliseconds) to wait for the lock, -1 infinity
344+
* @param expireAfter the time-to-live(milliseconds) for the lock status data
337345
* @return true if the lock was acquired and false if the waiting time elapsed before the lock was acquired
338346
* @throws InterruptedException –
339347
* if the current thread is interrupted while acquiring the lock (and interruption of lock acquisition is supported)
340348
*/
341-
protected abstract boolean tryRedisLockInner(long time) throws ExecutionException, InterruptedException;
349+
protected abstract boolean tryRedisLockInner(long time, long expireAfter) throws ExecutionException, InterruptedException;
342350

343351
/**
344352
* Unlock the lock using the unlink method in redis.
@@ -352,10 +360,16 @@ public long getLockedAt() {
352360

353361
@Override
354362
public final void lock() {
363+
this.lock(RedisLockRegistry.this.expireAfter, TimeUnit.MILLISECONDS);
364+
}
365+
366+
@Override
367+
public void lock(long customTtl, TimeUnit customTtlUnit) {
355368
this.localLock.lock();
356369
while (true) {
357370
try {
358-
if (tryRedisLock(-1L)) {
371+
long customTtlInMilliseconds = TimeUnit.MILLISECONDS.convert(customTtl, customTtlUnit);
372+
if (tryRedisLock(-1L, customTtlInMilliseconds)) {
359373
return;
360374
}
361375
}
@@ -382,7 +396,7 @@ public final void lockInterruptibly() throws InterruptedException {
382396
this.localLock.lockInterruptibly();
383397
while (true) {
384398
try {
385-
if (tryRedisLock(-1L)) {
399+
if (tryRedisLock(-1L, RedisLockRegistry.this.expireAfter)) {
386400
return;
387401
}
388402
}
@@ -411,12 +425,18 @@ public final boolean tryLock() {
411425

412426
@Override
413427
public final boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
428+
return this.tryLock(time, unit, RedisLockRegistry.this.expireAfter, TimeUnit.MILLISECONDS);
429+
}
430+
431+
@Override
432+
public boolean tryLock(long time, TimeUnit unit, long customTtl, TimeUnit customTtlUnit) throws InterruptedException {
414433
if (!this.localLock.tryLock(time, unit)) {
415434
return false;
416435
}
417436
try {
418437
long waitTime = TimeUnit.MILLISECONDS.convert(time, unit);
419-
boolean acquired = tryRedisLock(waitTime);
438+
long customTtlInMilliseconds = TimeUnit.MILLISECONDS.convert(customTtl, customTtlUnit);
439+
boolean acquired = tryRedisLock(waitTime, customTtlInMilliseconds);
420440
if (!acquired) {
421441
this.localLock.unlock();
422442
}
@@ -429,8 +449,8 @@ public final boolean tryLock(long time, TimeUnit unit) throws InterruptedExcepti
429449
return false;
430450
}
431451

432-
private boolean tryRedisLock(long time) throws ExecutionException, InterruptedException {
433-
final boolean acquired = tryRedisLockInner(time);
452+
private boolean tryRedisLock(long time, long expireAfter) throws ExecutionException, InterruptedException {
453+
final boolean acquired = tryRedisLockInner(time, expireAfter);
434454
if (acquired) {
435455
if (LOGGER.isDebugEnabled()) {
436456
LOGGER.debug("Acquired lock; " + this);
@@ -440,11 +460,11 @@ private boolean tryRedisLock(long time) throws ExecutionException, InterruptedEx
440460
return acquired;
441461
}
442462

443-
protected final Boolean obtainLock() {
463+
protected final Boolean obtainLock(long expireAfter) {
444464
return RedisLockRegistry.this.redisTemplate
445465
.execute(OBTAIN_LOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey),
446466
RedisLockRegistry.this.clientId,
447-
String.valueOf(RedisLockRegistry.this.expireAfter));
467+
String.valueOf(expireAfter));
448468
}
449469

450470
@Override
@@ -598,8 +618,8 @@ private RedisPubSubLock(String path) {
598618
}
599619

600620
@Override
601-
protected boolean tryRedisLockInner(long time) throws ExecutionException, InterruptedException {
602-
return subscribeLock(time);
621+
protected boolean tryRedisLockInner(long time, long expireAfter) throws ExecutionException, InterruptedException {
622+
return subscribeLock(time, expireAfter);
603623
}
604624

605625
@Override
@@ -618,9 +638,9 @@ private boolean removeLockKeyWithScript(RedisScript<Boolean> redisScript) {
618638
RedisLockRegistry.this.clientId, RedisLockRegistry.this.unLockChannelKey));
619639
}
620640

621-
private boolean subscribeLock(long time) throws ExecutionException, InterruptedException {
641+
private boolean subscribeLock(long time, long expireAfter) throws ExecutionException, InterruptedException {
622642
final long expiredTime = System.currentTimeMillis() + time;
623-
if (obtainLock()) {
643+
if (obtainLock(expireAfter)) {
624644
return true;
625645
}
626646

@@ -635,7 +655,7 @@ private boolean subscribeLock(long time) throws ExecutionException, InterruptedE
635655
Future<String> future =
636656
RedisLockRegistry.this.unlockNotifyMessageListener.subscribeLock(this.lockKey);
637657
//DCL
638-
if (obtainLock()) {
658+
if (obtainLock(expireAfter)) {
639659
return true;
640660
}
641661
try {
@@ -645,7 +665,7 @@ private boolean subscribeLock(long time) throws ExecutionException, InterruptedE
645665
}
646666
catch (TimeoutException ignore) {
647667
}
648-
if (obtainLock()) {
668+
if (obtainLock(expireAfter)) {
649669
return true;
650670
}
651671
}
@@ -737,18 +757,18 @@ private RedisSpinLock(String path) {
737757
}
738758

739759
@Override
740-
protected boolean tryRedisLockInner(long time) throws InterruptedException {
760+
protected boolean tryRedisLockInner(long time, long expireAfter) throws InterruptedException {
741761
long now = System.currentTimeMillis();
742762
if (time == -1L) {
743-
while (!obtainLock()) {
763+
while (!obtainLock(expireAfter)) {
744764
Thread.sleep(100); //NOSONAR
745765
}
746766
return true;
747767
}
748768
else {
749769
long expire = now + TimeUnit.MILLISECONDS.convert(time, TimeUnit.MILLISECONDS);
750770
boolean acquired;
751-
while (!(acquired = obtainLock()) && System.currentTimeMillis() < expire) { //NOSONAR
771+
while (!(acquired = obtainLock(expireAfter)) && System.currentTimeMillis() < expire) { //NOSONAR
752772
Thread.sleep(100); //NOSONAR
753773
}
754774
return acquired;

Diff for: spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java

+61
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@
4949
import org.springframework.data.redis.core.StringRedisTemplate;
5050
import org.springframework.integration.redis.RedisContainerTest;
5151
import org.springframework.integration.redis.util.RedisLockRegistry.RedisLockType;
52+
import org.springframework.integration.support.locks.CustomTtlLock;
5253
import org.springframework.integration.test.util.TestUtils;
5354

5455
import static org.assertj.core.api.Assertions.assertThat;
5556
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
5657
import static org.assertj.core.api.Assertions.assertThatNoException;
58+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5759
import static org.mockito.Mockito.mock;
5860

5961
/**
@@ -64,6 +66,7 @@
6466
* @author Unseok Kim
6567
* @author Artem Vozhdayenko
6668
* @author Anton Gabov
69+
* @author Eddie Cho
6770
*
6871
* @since 4.0
6972
*
@@ -115,6 +118,64 @@ void testLock(RedisLockType testRedisLockType) {
115118
registry.destroy();
116119
}
117120

121+
@ParameterizedTest
122+
@EnumSource(RedisLockType.class)
123+
void testLockWithCustomTtl(RedisLockType testRedisLockType) throws InterruptedException {
124+
RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey, 100);
125+
registry.setRedisLockType(testRedisLockType);
126+
for (int i = 0; i < 3; i++) {
127+
CustomTtlLock lock = registry.obtainCustomTtlLock("foo");
128+
lock.lock(500, TimeUnit.MILLISECONDS);
129+
try {
130+
assertThat(getRedisLockRegistryLocks(registry)).hasSize(1);
131+
Thread.sleep(400);
132+
}
133+
finally {
134+
lock.unlock();
135+
}
136+
}
137+
registry.expireUnusedOlderThan(-1000);
138+
assertThat(getRedisLockRegistryLocks(registry)).isEmpty();
139+
registry.destroy();
140+
}
141+
142+
@ParameterizedTest
143+
@EnumSource(RedisLockType.class)
144+
void testTryLockWithCustomTtl(RedisLockType testRedisLockType) throws InterruptedException {
145+
RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey, 100);
146+
registry.setRedisLockType(testRedisLockType);
147+
for (int i = 0; i < 3; i++) {
148+
CustomTtlLock lock = registry.obtainCustomTtlLock("foo");
149+
lock.tryLock(100, TimeUnit.MILLISECONDS, 500, TimeUnit.MILLISECONDS);
150+
try {
151+
assertThat(getRedisLockRegistryLocks(registry)).hasSize(1);
152+
Thread.sleep(400);
153+
}
154+
finally {
155+
lock.unlock();
156+
}
157+
}
158+
registry.expireUnusedOlderThan(-1000);
159+
assertThat(getRedisLockRegistryLocks(registry)).isEmpty();
160+
registry.destroy();
161+
}
162+
163+
@ParameterizedTest
164+
@EnumSource(RedisLockType.class)
165+
void testUnlock_lockStatusIsExpired_IllegalStateExceptionWillBeThrown(RedisLockType testRedisLockType) throws InterruptedException {
166+
RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey, 100);
167+
registry.setRedisLockType(testRedisLockType);
168+
Lock lock = registry.obtain("foo");
169+
try {
170+
lock.lock();
171+
Thread.sleep(200);
172+
}
173+
finally {
174+
assertThatThrownBy(lock::unlock).isInstanceOf(IllegalStateException.class);
175+
}
176+
registry.destroy();
177+
}
178+
118179
@ParameterizedTest
119180
@EnumSource(RedisLockType.class)
120181
void testLockInterruptibly(RedisLockType testRedisLockType) throws Exception {

0 commit comments

Comments
 (0)