Skip to content

Commit bdce2b9

Browse files
AntonGabovartembilan
authored andcommitted
RedisLockRegistry: Don't expire not acquired lock
Fix race condition, when methods `RedisLockRegistry#expireUnusedOlderThan` and `RedisLockRegistry#obtain` are executed successively. It's possible to delete the lock from `RedisLockRegistry#expireUnusedOlderThan` method, when lock is created but is not acquired (`RedisLock#getLockedAt = 0`) It can lead to the situation, when `RedisLockRegistry#obtain` returns multiple locks with the same redis-key, which shouldn't happen at all. * Skip locks from expiration when their `lockedAt == 0` - new, not acquired yet. **Cherry-pick to `6.0.x` & `5.5.x`** # Conflicts: # spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java
1 parent efab65b commit bdce2b9

File tree

2 files changed

+65
-20
lines changed

2 files changed

+65
-20
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
* @author Artem Bilan
8282
* @author Vedran Pavic
8383
* @author Unseok Kim
84+
* @author Anton Gabov
8485
*
8586
* @since 4.0
8687
*
@@ -235,7 +236,11 @@ public void expireUnusedOlderThan(long age) {
235236
this.locks.entrySet()
236237
.removeIf(entry -> {
237238
RedisLock lock = entry.getValue();
238-
return now - lock.getLockedAt() > age && !lock.isAcquiredInThisProcess();
239+
long lockedAt = lock.getLockedAt();
240+
return now - lockedAt > age
241+
// 'lockedAt = 0' means that the lock is still not acquired!
242+
&& lockedAt > 0
243+
&& !lock.isAcquiredInThisProcess();
239244
});
240245
}
241246
}

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

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.util.Collection;
2020
import java.util.List;
2121
import java.util.Map;
22-
import java.util.Properties;
2322
import java.util.Queue;
2423
import java.util.UUID;
2524
import java.util.concurrent.Callable;
@@ -33,6 +32,7 @@
3332
import java.util.concurrent.TimeUnit;
3433
import java.util.concurrent.atomic.AtomicBoolean;
3534
import java.util.concurrent.atomic.AtomicInteger;
35+
import java.util.concurrent.atomic.AtomicReference;
3636
import java.util.concurrent.locks.Lock;
3737
import java.util.stream.Collectors;
3838
import java.util.stream.IntStream;
@@ -47,8 +47,6 @@
4747
import org.junit.runners.Parameterized.Parameters;
4848

4949
import org.springframework.data.redis.connection.RedisConnectionFactory;
50-
import org.springframework.data.redis.core.RedisCallback;
51-
import org.springframework.data.redis.core.RedisOperations;
5250
import org.springframework.data.redis.core.StringRedisTemplate;
5351
import org.springframework.integration.redis.rules.RedisAvailable;
5452
import org.springframework.integration.redis.rules.RedisAvailableTests;
@@ -57,16 +55,14 @@
5755

5856
import static org.assertj.core.api.Assertions.assertThat;
5957
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
60-
import static org.mockito.ArgumentMatchers.any;
61-
import static org.mockito.BDDMockito.willReturn;
62-
import static org.mockito.Mockito.mock;
6358

6459
/**
6560
* @author Gary Russell
6661
* @author Konstantin Yakimov
6762
* @author Artem Bilan
6863
* @author Vedran Pavic
6964
* @author Unseok Kim
65+
* @author Anton Gabov
7066
*
7167
* @since 4.0
7268
*
@@ -824,21 +820,65 @@ public void earlyWakeUpTest() throws InterruptedException {
824820
registry3.destroy();
825821
}
826822

827-
828-
@SuppressWarnings({ "unchecked", "rawtypes" })
829823
@Test
830-
public void testUlink() {
831-
RedisOperations ops = mock(RedisOperations.class);
832-
Properties props = new Properties();
833-
willReturn(props).given(ops).execute(any(RedisCallback.class));
834-
props.setProperty("redis_version", "3.0.0");
835-
RedisLockRegistry registry = new RedisLockRegistry(mock(RedisConnectionFactory.class), "foo");
836-
registry.setRedisLockType(testRedisLockType);
837-
assertThat(TestUtils.getPropertyValue(registry, "ulinkAvailable", Boolean.class)).isFalse();
838-
props.setProperty("redis_version", "4.0.0");
839-
registry = new RedisLockRegistry(mock(RedisConnectionFactory.class), "foo");
824+
@RedisAvailable
825+
public void testTwoThreadsRemoveAndObtainSameLockSimultaneously() throws Exception {
826+
final int TEST_CNT = 200;
827+
final long EXPIRATION_TIME_MILLIS = 10000;
828+
final long LOCK_WAIT_TIME_MILLIS = 500;
829+
final String testKey = "testKey";
830+
831+
final RedisLockRegistry registry = new RedisLockRegistry(getConnectionFactoryForTest(), this.registryKey);
840832
registry.setRedisLockType(testRedisLockType);
841-
assertThat(TestUtils.getPropertyValue(registry, "ulinkAvailable", Boolean.class)).isTrue();
833+
834+
for (int i = 0; i < TEST_CNT; i++) {
835+
final String lockKey = testKey + i;
836+
final CountDownLatch latch = new CountDownLatch(1);
837+
final AtomicReference<Lock> lock1 = new AtomicReference<>();
838+
final AtomicReference<Lock> lock2 = new AtomicReference<>();
839+
840+
Thread thread1 = new Thread(() -> {
841+
try {
842+
latch.await();
843+
// remove lock
844+
registry.expireUnusedOlderThan(EXPIRATION_TIME_MILLIS);
845+
// obtain new lock and try to acquire
846+
Lock lock = registry.obtain(lockKey);
847+
lock.tryLock(LOCK_WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS);
848+
lock.unlock();
849+
850+
lock1.set(lock);
851+
}
852+
catch (InterruptedException ignore) {
853+
}
854+
});
855+
856+
Thread thread2 = new Thread(() -> {
857+
try {
858+
latch.await();
859+
// remove lock
860+
registry.expireUnusedOlderThan(EXPIRATION_TIME_MILLIS);
861+
// obtain new lock and try to acquire
862+
Lock lock = registry.obtain(lockKey);
863+
lock.tryLock(LOCK_WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS);
864+
lock.unlock();
865+
866+
lock2.set(lock);
867+
}
868+
catch (InterruptedException ignore) {
869+
}
870+
});
871+
872+
thread1.start();
873+
thread2.start();
874+
latch.countDown();
875+
thread1.join();
876+
thread2.join();
877+
878+
// locks must be the same!
879+
assertThat(lock1.get()).isEqualTo(lock2.get());
880+
}
881+
842882
registry.destroy();
843883
}
844884

0 commit comments

Comments
 (0)