Skip to content

Commit 0a11f3b

Browse files
committed
Apply additional polishing to Async Caching support.
Cleanup ambigious, unreadable, unstructured and complex logic in DefaultRedisCacheWriter. Split RedisCacheTests.retrieveCacheValueUsingJedis() test logic by test case. Edit Javadoc. See spring-projects#2650 Original pull request: spring-projects#2717
1 parent 19b59e2 commit 0a11f3b

File tree

7 files changed

+613
-255
lines changed

7 files changed

+613
-255
lines changed

Diff for: src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java

+111-90
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,19 @@
1515
*/
1616
package org.springframework.data.redis.cache;
1717

18-
import reactor.core.publisher.Flux;
19-
import reactor.core.publisher.Mono;
20-
2118
import java.nio.ByteBuffer;
2219
import java.nio.charset.StandardCharsets;
2320
import java.time.Duration;
2421
import java.util.concurrent.CompletableFuture;
2522
import java.util.concurrent.TimeUnit;
2623
import java.util.concurrent.atomic.AtomicLong;
24+
import java.util.function.Consumer;
2725
import java.util.function.Function;
2826

2927
import org.springframework.dao.PessimisticLockingFailureException;
3028
import org.springframework.data.redis.connection.ReactiveRedisConnection;
3129
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
30+
import org.springframework.data.redis.connection.ReactiveStringCommands;
3231
import org.springframework.data.redis.connection.RedisConnection;
3332
import org.springframework.data.redis.connection.RedisConnectionFactory;
3433
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
@@ -39,18 +38,22 @@
3938
import org.springframework.util.ClassUtils;
4039
import org.springframework.util.ObjectUtils;
4140

41+
import reactor.core.publisher.Flux;
42+
import reactor.core.publisher.Mono;
43+
import reactor.core.publisher.SignalType;
44+
4245
/**
4346
* {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
4447
* and {@literal cluster} environments, and uses a given {@link RedisConnectionFactory} to obtain the actual
4548
* {@link RedisConnection}.
4649
* <p>
4750
* {@link DefaultRedisCacheWriter} can be used in
4851
* {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} or
49-
* {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While
50-
* {@literal non-locking} aims for maximum performance it may result in overlapping, non-atomic, command execution for
51-
* operations spanning multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents
52-
* command overlap by setting an explicit lock key and checking against presence of this key which leads to additional
53-
* requests and potential command wait times.
52+
* {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While {@literal non-locking}
53+
* aims for maximum performance it may result in overlapping, non-atomic, command execution for operations spanning
54+
* multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents command overlap
55+
* by setting an explicit lock key and checking against presence of this key which leads to additional requests
56+
* and potential command wait times.
5457
*
5558
* @author Christoph Strobl
5659
* @author Mark Paluch
@@ -60,8 +63,8 @@
6063
*/
6164
class DefaultRedisCacheWriter implements RedisCacheWriter {
6265

63-
private static final boolean REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT = ClassUtils
64-
.isPresent("org.springframework.data.redis.connection.ReactiveRedisConnectionFactory", null);
66+
private static final boolean REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT =
67+
ClassUtils.isPresent("org.springframework.data.redis.connection.ReactiveRedisConnectionFactory", null);
6568

6669
private final BatchStrategy batchStrategy;
6770

@@ -75,31 +78,21 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
7578

7679
private final AsyncCacheWriter asyncCacheWriter;
7780

78-
/**
79-
* @param connectionFactory must not be {@literal null}.
80-
* @param batchStrategy must not be {@literal null}.
81-
*/
8281
DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, BatchStrategy batchStrategy) {
8382
this(connectionFactory, Duration.ZERO, batchStrategy);
8483
}
8584

8685
/**
87-
* @param connectionFactory must not be {@literal null}.
88-
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
89-
* to disable locking.
90-
* @param batchStrategy must not be {@literal null}.
86+
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}.
87+
* Use {@link Duration#ZERO} to disable locking.
9188
*/
9289
DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime, BatchStrategy batchStrategy) {
9390
this(connectionFactory, sleepTime, TtlFunction.persistent(), CacheStatisticsCollector.none(), batchStrategy);
9491
}
9592

9693
/**
97-
* @param connectionFactory must not be {@literal null}.
98-
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
99-
* to disable locking.
100-
* @param lockTtl Lock TTL function must not be {@literal null}.
101-
* @param cacheStatisticsCollector must not be {@literal null}.
102-
* @param batchStrategy must not be {@literal null}.
94+
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}.
95+
* Use {@link Duration#ZERO} to disable locking.
10396
*/
10497
DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime, TtlFunction lockTtl,
10598
CacheStatisticsCollector cacheStatisticsCollector, BatchStrategy batchStrategy) {
@@ -160,19 +153,19 @@ public CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Dur
160153
Assert.notNull(name, "Name must not be null");
161154
Assert.notNull(key, "Key must not be null");
162155

163-
return asyncCacheWriter.retrieve(name, key, ttl) //
164-
.thenApply(cachedValue -> {
156+
return asyncCacheWriter.retrieve(name, key, ttl).thenApply(cachedValue -> {
165157

166-
statistics.incGets(name);
158+
statistics.incGets(name);
167159

168-
if (cachedValue != null) {
169-
statistics.incHits(name);
170-
} else {
171-
statistics.incMisses(name);
172-
}
160+
if (cachedValue != null) {
161+
statistics.incHits(name);
162+
}
163+
else {
164+
statistics.incMisses(name);
165+
}
173166

174-
return cachedValue;
175-
});
167+
return cachedValue;
168+
});
176169
}
177170

178171
@Override
@@ -185,8 +178,8 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
185178
execute(name, connection -> {
186179

187180
if (shouldExpireWithin(ttl)) {
188-
connection.stringCommands().set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS),
189-
SetOption.upsert());
181+
connection.stringCommands()
182+
.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
190183
} else {
191184
connection.stringCommands().set(key, value);
192185
}
@@ -204,8 +197,7 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
204197
Assert.notNull(key, "Key must not be null");
205198
Assert.notNull(value, "Value must not be null");
206199

207-
return asyncCacheWriter.store(name, key, value, ttl) //
208-
.thenRun(() -> statistics.incPuts(name));
200+
return asyncCacheWriter.store(name, key, value, ttl).thenRun(() -> statistics.incPuts(name));
209201
}
210202

211203
@Override
@@ -226,8 +218,8 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat
226218
boolean put;
227219

228220
if (shouldExpireWithin(ttl)) {
229-
put = ObjectUtils.nullSafeEquals(
230-
connection.stringCommands().set(key, value, Expiration.from(ttl), SetOption.ifAbsent()), true);
221+
put = ObjectUtils.nullSafeEquals(connection.stringCommands()
222+
.set(key, value, Expiration.from(ttl), SetOption.ifAbsent()), true);
231223
} else {
232224
put = ObjectUtils.nullSafeEquals(connection.stringCommands().setNX(key, value), true);
233225
}
@@ -377,12 +369,10 @@ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection c
377369
Thread.sleep(this.sleepTime.toMillis());
378370
}
379371
} catch (InterruptedException cause) {
380-
381372
// Re-interrupt current Thread to allow other participants to react.
382373
Thread.currentThread().interrupt();
383-
384-
throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name),
385-
cause);
374+
String message = String.format("Interrupted while waiting to unlock cache %s", name);
375+
throw new PessimisticLockingFailureException(message, cause);
386376
} finally {
387377
this.statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs);
388378
}
@@ -418,8 +408,8 @@ interface AsyncCacheWriter {
418408
* @param name the cache name from which to retrieve the cache entry.
419409
* @param key the cache entry key.
420410
* @param ttl optional TTL to set for Time-to-Idle eviction.
421-
* @return a future that completes either with a value if the value exists or completing with {@code null} if the
422-
* cache does not contain an entry.
411+
* @return a future that completes either with a value if the value exists or completing with {@code null}
412+
* if the cache does not contain an entry.
423413
*/
424414
CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Duration ttl);
425415

@@ -433,6 +423,7 @@ interface AsyncCacheWriter {
433423
* @return a future that signals completion.
434424
*/
435425
CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nullable Duration ttl);
426+
436427
}
437428

438429
/**
@@ -441,6 +432,7 @@ interface AsyncCacheWriter {
441432
* @since 3.2
442433
*/
443434
enum UnsupportedAsyncCacheWriter implements AsyncCacheWriter {
435+
444436
INSTANCE;
445437

446438
@Override
@@ -460,8 +452,8 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
460452
}
461453

462454
/**
463-
* Delegate implementing {@link AsyncCacheWriter} to provide asynchronous cache retrieval and storage operations using
464-
* {@link ReactiveRedisConnectionFactory}.
455+
* Delegate implementing {@link AsyncCacheWriter} to provide asynchronous cache retrieval and storage operations
456+
* using {@link ReactiveRedisConnectionFactory}.
465457
*
466458
* @since 3.2
467459
*/
@@ -478,19 +470,16 @@ public CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Dur
478470
return doWithConnection(connection -> {
479471

480472
ByteBuffer wrappedKey = ByteBuffer.wrap(key);
481-
Mono<?> cacheLockCheckFlux;
482473

483-
if (isLockingCacheWriter())
484-
cacheLockCheckFlux = waitForLock(connection, name);
485-
else {
486-
cacheLockCheckFlux = Mono.empty();
487-
}
474+
Mono<?> cacheLockCheck = isLockingCacheWriter() ? waitForLock(connection, name) : Mono.empty();
475+
476+
ReactiveStringCommands stringCommands = connection.stringCommands();
488477

489478
Mono<ByteBuffer> get = shouldExpireWithin(ttl)
490-
? connection.stringCommands().getEx(wrappedKey, Expiration.from(ttl))
491-
: connection.stringCommands().get(wrappedKey);
479+
? stringCommands.getEx(wrappedKey, toExpiration(ttl))
480+
: stringCommands.get(wrappedKey);
492481

493-
return cacheLockCheckFlux.then(get).map(ByteUtils::getBytes).toFuture();
482+
return cacheLockCheck.then(get).map(ByteUtils::getBytes).toFuture();
494483
});
495484
}
496485

@@ -499,15 +488,9 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
499488

500489
return doWithConnection(connection -> {
501490

502-
Mono<?> mono;
503-
504-
if (isLockingCacheWriter()) {
505-
506-
mono = Mono.usingWhen(doLock(name, key, value, connection), unused -> doStore(key, value, ttl, connection),
507-
unused -> doUnlock(name, connection));
508-
} else {
509-
mono = doStore(key, value, ttl, connection);
510-
}
491+
Mono<?> mono = isLockingCacheWriter()
492+
? doLockStoreUnlock(name, key, value, ttl, connection)
493+
: doStore(key, value, ttl, connection);
511494

512495
return mono.then().toFuture();
513496
});
@@ -519,24 +502,31 @@ private Mono<Boolean> doStore(byte[] cacheKey, byte[] value, @Nullable Duration
519502
ByteBuffer wrappedKey = ByteBuffer.wrap(cacheKey);
520503
ByteBuffer wrappedValue = ByteBuffer.wrap(value);
521504

522-
if (shouldExpireWithin(ttl)) {
523-
return connection.stringCommands().set(wrappedKey, wrappedValue,
524-
Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
525-
} else {
526-
return connection.stringCommands().set(wrappedKey, wrappedValue);
527-
}
505+
ReactiveStringCommands stringCommands = connection.stringCommands();
506+
507+
return shouldExpireWithin(ttl)
508+
? stringCommands.set(wrappedKey, wrappedValue, toExpiration(ttl), SetOption.upsert())
509+
: stringCommands.set(wrappedKey, wrappedValue);
510+
}
511+
512+
private Mono<Boolean> doLockStoreUnlock(String name, byte[] key, byte[] value, @Nullable Duration ttl,
513+
ReactiveRedisConnection connection) {
514+
515+
return Mono.usingWhen(doLock(name, key, value, connection), unused -> doStore(key, value, ttl, connection),
516+
unused -> doUnlock(name, connection));
528517
}
529518

530519
private Mono<Object> doLock(String name, Object contextualKey, @Nullable Object contextualValue,
531520
ReactiveRedisConnection connection) {
532521

533-
Expiration expiration = Expiration.from(lockTtl.getTimeToLive(contextualKey, contextualValue));
522+
ByteBuffer key = ByteBuffer.wrap(createCacheLockKey(name));
523+
ByteBuffer value = ByteBuffer.wrap(new byte[0]);
524+
525+
Expiration expiration = toExpiration(contextualKey, contextualValue);
534526

535-
return connection.stringCommands()
536-
.set(ByteBuffer.wrap(createCacheLockKey(name)), ByteBuffer.wrap(new byte[0]), expiration,
537-
SetOption.SET_IF_ABSENT) //
538-
.thenReturn(new Object()); // Ensure we emit an object, otherwise, the Mono.usingWhen operator doesn't run
539-
// the inner resource function.
527+
return connection.stringCommands().set(key, value, expiration, SetOption.SET_IF_ABSENT) //
528+
// Ensure we emit an object, otherwise, the Mono.usingWhen operator doesn't run the inner resource function.
529+
.thenReturn(Boolean.TRUE);
540530
}
541531

542532
private Mono<Void> doUnlock(String name, ReactiveRedisConnection connection) {
@@ -545,28 +535,59 @@ private Mono<Void> doUnlock(String name, ReactiveRedisConnection connection) {
545535

546536
private Mono<Void> waitForLock(ReactiveRedisConnection connection, String cacheName) {
547537

548-
AtomicLong lockWaitTimeNs = new AtomicLong();
549-
byte[] cacheLockKey = createCacheLockKey(cacheName);
538+
AtomicLong lockWaitNanoTime = new AtomicLong();
539+
540+
Consumer<org.reactivestreams.Subscription> setNanoTimeOnLockWait = subscription ->
541+
lockWaitNanoTime.set(System.nanoTime());
550542

551-
Flux<Long> wait = Flux.interval(Duration.ZERO, sleepTime);
552-
Mono<Boolean> exists = connection.keyCommands().exists(ByteBuffer.wrap(cacheLockKey)).filter(it -> !it);
543+
Consumer<SignalType> recordStatistics = signalType ->
544+
statistics.incLockTime(cacheName, System.nanoTime() - lockWaitNanoTime.get());
553545

554-
return wait.doOnSubscribe(subscription -> lockWaitTimeNs.set(System.nanoTime())) //
555-
.flatMap(it -> exists) //
556-
.doFinally(signalType -> statistics.incLockTime(cacheName, System.nanoTime() - lockWaitTimeNs.get())) //
546+
Function<Long, Mono<Boolean>> doCacheLockExistsCheck = lockWaitTime -> connection.keyCommands()
547+
.exists(toCacheLockKey(cacheName)).filter(cacheLockKeyExists -> !cacheLockKeyExists);
548+
549+
return waitForLock() //
550+
.doOnSubscribe(setNanoTimeOnLockWait) //
551+
.flatMap(doCacheLockExistsCheck) //
552+
.doFinally(recordStatistics) //
557553
.next() //
558554
.then();
559555
}
560556

557+
private Flux<Long> waitForLock() {
558+
return Flux.interval(Duration.ZERO, sleepTime);
559+
}
560+
561+
private ByteBuffer toCacheLockKey(String cacheName) {
562+
return ByteBuffer.wrap(createCacheLockKey(cacheName));
563+
}
564+
565+
private Expiration toExpiration(Duration ttl) {
566+
return Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS);
567+
}
568+
569+
private Expiration toExpiration(Object contextualKey, @Nullable Object contextualValue) {
570+
return Expiration.from(lockTtl.getTimeToLive(contextualKey, contextualValue));
571+
}
572+
573+
private ReactiveRedisConnectionFactory getReactiveConnectionFactory() {
574+
return (ReactiveRedisConnectionFactory) DefaultRedisCacheWriter.this.connectionFactory;
575+
}
576+
577+
private Mono<ReactiveRedisConnection> getReactiveConnection() {
578+
return Mono.fromSupplier(getReactiveConnectionFactory()::getReactiveConnection);
579+
}
580+
561581
private <T> CompletableFuture<T> doWithConnection(
562582
Function<ReactiveRedisConnection, CompletableFuture<T>> callback) {
563583

564-
ReactiveRedisConnectionFactory cf = (ReactiveRedisConnectionFactory) connectionFactory;
584+
Function<ReactiveRedisConnection, Mono<T>> commandExecution = connection ->
585+
Mono.fromCompletionStage(callback.apply(connection));
586+
587+
Mono<T> result = Mono.usingWhen(getReactiveConnection(), commandExecution,
588+
ReactiveRedisConnection::closeLater);
565589

566-
return Mono.usingWhen(Mono.fromSupplier(cf::getReactiveConnection), //
567-
it -> Mono.fromCompletionStage(callback.apply(it)), //
568-
ReactiveRedisConnection::closeLater) //
569-
.toFuture();
590+
return result.toFuture();
570591
}
571592
}
572593
}

0 commit comments

Comments
 (0)