Skip to content

Commit 0948d0d

Browse files
committed
Polishing.
Refine unlocking by checking whether the lock was actually applied. Reduce allocations, refine test assertions to check for concurrency. See #1686 Original pull request: #2879
1 parent 3cf7cbf commit 0948d0d

File tree

2 files changed

+61
-24
lines changed

2 files changed

+61
-24
lines changed

src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java

+13-11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.redis.connection.ReactiveStringCommands;
3333
import org.springframework.data.redis.connection.RedisConnection;
3434
import org.springframework.data.redis.connection.RedisConnectionFactory;
35+
import org.springframework.data.redis.connection.RedisStringCommands;
3536
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
3637
import org.springframework.data.redis.core.types.Expiration;
3738
import org.springframework.data.redis.util.ByteUtils;
@@ -219,8 +220,10 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat
219220

220221
return execute(name, connection -> {
221222

223+
boolean wasLocked = false;
222224
if (isLockingCacheWriter()) {
223225
doLock(name, key, value, connection);
226+
wasLocked = true;
224227
}
225228

226229
try {
@@ -242,7 +245,7 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat
242245
return connection.stringCommands().get(key);
243246

244247
} finally {
245-
if (isLockingCacheWriter()) {
248+
if (isLockingCacheWriter() && wasLocked) {
246249
doUnlock(name, connection);
247250
}
248251
}
@@ -319,15 +322,17 @@ void lock(String name) {
319322
execute(name, connection -> doLock(name, name, null, connection));
320323
}
321324

322-
@Nullable
323-
protected Boolean doLock(String name, Object contextualKey, @Nullable Object contextualValue,
324-
RedisConnection connection) {
325+
boolean doLock(String name, Object contextualKey, @Nullable Object contextualValue, RedisConnection connection) {
325326

327+
RedisStringCommands commands = connection.stringCommands();
326328
Expiration expiration = Expiration.from(this.lockTtl.getTimeToLive(contextualKey, contextualValue));
329+
byte[] cacheLockKey = createCacheLockKey(name);
327330

328-
while (!ObjectUtils.nullSafeEquals(connection.stringCommands().set(createCacheLockKey(name), new byte[0], expiration, SetOption.SET_IF_ABSENT),true)) {
331+
while (!ObjectUtils.nullSafeEquals(commands.set(cacheLockKey, new byte[0], expiration, SetOption.SET_IF_ABSENT),
332+
true)) {
329333
checkAndPotentiallyWaitUntilUnlocked(name, connection);
330334
}
335+
331336
return true;
332337
}
333338

@@ -341,7 +346,7 @@ void unlock(String name) {
341346
}
342347

343348
@Nullable
344-
private Long doUnlock(String name, RedisConnection connection) {
349+
Long doUnlock(String name, RedisConnection connection) {
345350
return connection.keyCommands().del(createCacheLockKey(name));
346351
}
347352

@@ -489,8 +494,7 @@ public CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Dur
489494
Mono<?> cacheLockCheck = isLockingCacheWriter() ? waitForLock(connection, name) : Mono.empty();
490495
ReactiveStringCommands stringCommands = connection.stringCommands();
491496

492-
Mono<ByteBuffer> get = shouldExpireWithin(ttl)
493-
? stringCommands.getEx(wrappedKey, Expiration.from(ttl))
497+
Mono<ByteBuffer> get = shouldExpireWithin(ttl) ? stringCommands.getEx(wrappedKey, Expiration.from(ttl))
494498
: stringCommands.get(wrappedKey);
495499

496500
return cacheLockCheck.then(get).map(ByteUtils::getBytes).toFuture();
@@ -502,8 +506,7 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
502506

503507
return doWithConnection(connection -> {
504508

505-
Mono<?> mono = isLockingCacheWriter()
506-
? doStoreWithLocking(name, key, value, ttl, connection)
509+
Mono<?> mono = isLockingCacheWriter() ? doStoreWithLocking(name, key, value, ttl, connection)
507510
: doStore(key, value, ttl, connection);
508511

509512
return mono.then().toFuture();
@@ -531,7 +534,6 @@ private Mono<Boolean> doStore(byte[] cacheKey, byte[] value, @Nullable Duration
531534
}
532535
}
533536

534-
535537
private Mono<Object> doLock(String name, Object contextualKey, @Nullable Object contextualValue,
536538
ReactiveRedisConnection connection) {
537539

src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java

+48-13
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@
2121
import java.nio.charset.Charset;
2222
import java.nio.charset.StandardCharsets;
2323
import java.time.Duration;
24+
import java.util.ArrayList;
2425
import java.util.Collection;
26+
import java.util.List;
27+
import java.util.concurrent.CompletableFuture;
2528
import java.util.concurrent.CountDownLatch;
2629
import java.util.concurrent.ExecutionException;
2730
import java.util.concurrent.TimeUnit;
31+
import java.util.concurrent.atomic.AtomicLong;
2832
import java.util.concurrent.atomic.AtomicReference;
2933
import java.util.function.Consumer;
3034

3135
import org.junit.jupiter.api.BeforeEach;
36+
3237
import org.springframework.data.redis.connection.RedisConnection;
3338
import org.springframework.data.redis.connection.RedisConnectionFactory;
3439
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
@@ -421,43 +426,73 @@ void noOpStatisticsCollectorReturnsEmptyStatsInstance() {
421426
assertThat(stats.getPuts()).isZero();
422427
}
423428

424-
@ParameterizedRedisTest
429+
@ParameterizedRedisTest // GH-1686
425430
void doLockShouldGetLock() throws InterruptedException {
426431

427432
int threadCount = 3;
428433
CountDownLatch beforeWrite = new CountDownLatch(threadCount);
429434
CountDownLatch afterWrite = new CountDownLatch(threadCount);
435+
AtomicLong concurrency = new AtomicLong();
430436

431-
DefaultRedisCacheWriter cw = new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50),
432-
BatchStrategies.keys()){
433-
@Nullable
434-
protected Boolean doLock(String name, Object contextualKey, @Nullable Object contextualValue,
435-
RedisConnection connection) {
436-
Boolean doLock = super.doLock(name, contextualKey, contextualValue, connection);
437-
assertThat(doLock).isTrue();
437+
DefaultRedisCacheWriter cw = new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(10),
438+
BatchStrategies.keys()) {
439+
440+
boolean doLock(String name, Object contextualKey, @Nullable Object contextualValue, RedisConnection connection) {
441+
442+
boolean doLock = super.doLock(name, contextualKey, contextualValue, connection);
443+
444+
// any concurrent access (aka not waiting until the lock is acquired) will result in a concurrency greater 1
445+
assertThat(concurrency.incrementAndGet()).isOne();
438446
return doLock;
439447
}
448+
449+
@Nullable
450+
@Override
451+
Long doUnlock(String name, RedisConnection connection) {
452+
try {
453+
return super.doUnlock(name, connection);
454+
} finally {
455+
concurrency.decrementAndGet();
456+
}
457+
}
440458
};
441459

442460
cw.lock(CACHE_NAME);
443461

462+
// introduce concurrency
463+
List<CompletableFuture<?>> completions = new ArrayList<>();
444464
for (int i = 0; i < threadCount; i++) {
465+
466+
CompletableFuture<?> completion = new CompletableFuture<>();
467+
completions.add(completion);
468+
445469
Thread th = new Thread(() -> {
446470
beforeWrite.countDown();
447-
cw.putIfAbsent(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
471+
try {
472+
cw.putIfAbsent(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
473+
completion.complete(null);
474+
} catch (Throwable e) {
475+
completion.completeExceptionally(e);
476+
}
448477
afterWrite.countDown();
449478
});
450479

451480
th.start();
452481
}
453482

454-
beforeWrite.await();
455-
456-
Thread.sleep(200);
483+
assertThat(beforeWrite.await(5, TimeUnit.SECONDS)).isTrue();
484+
Thread.sleep(100);
457485

458486
cw.unlock(CACHE_NAME);
459-
afterWrite.await();
487+
assertThat(afterWrite.await(5, TimeUnit.SECONDS)).isTrue();
460488

489+
for (CompletableFuture<?> completion : completions) {
490+
assertThat(completion).isCompleted().isCompletedWithValue(null);
491+
}
492+
493+
doWithConnection(conn -> {
494+
assertThat(conn.exists("default-redis-cache-writer-tests~lock".getBytes())).isFalse();
495+
});
461496
}
462497

463498
private void doWithConnection(Consumer<RedisConnection> callback) {

0 commit comments

Comments
 (0)