Handle low-level errors for sync/flux/mono/future gets

This change adds 3 protected methods to `AbstractCacheInvoker` that wrap
additional `Cache#retrieve` and `Cache#get` calls with
`handleCacheGetError` in case the Cache call itself fails.

For example, if the cache is remote and a connection to it cannot be
established.

Closes gh-21590
This commit is contained in:
Simon Baslé
2024-08-12 15:02:05 +02:00
parent 5e72ee36e2
commit 2eda5d7a2a
3 changed files with 126 additions and 5 deletions

View File

@@ -16,6 +16,10 @@
package org.springframework.cache.interceptor;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import org.springframework.cache.Cache;
import org.springframework.lang.Nullable;
import org.springframework.util.function.SingletonSupplier;
@@ -78,6 +82,52 @@ public abstract class AbstractCacheInvoker {
}
}
@Nullable
protected <T> T doGet(Cache cache, Object key, Callable<T> valueLoader) {
try {
return cache.get(key, valueLoader);
}
catch (Cache.ValueRetrievalException ex) {
throw ex;
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
try {
return valueLoader.call();
}
catch (Exception ex2) {
throw new RuntimeException(ex2);
}
}
}
@Nullable
protected CompletableFuture<?> doRetrieve(Cache cache, Object key) {
try {
return cache.retrieve(key);
}
catch (Cache.ValueRetrievalException ex) {
throw ex;
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
return null;
}
}
protected <T> CompletableFuture<T> doRetrieve(Cache cache, Object key, Supplier<CompletableFuture<T>> valueLoader) {
try {
return cache.retrieve(key, valueLoader);
}
catch (Cache.ValueRetrievalException ex) {
throw ex;
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
return valueLoader.get();
}
}
/**
* Execute {@link Cache#put(Object, Object)} on the specified {@link Cache}
* and invoke the error handler if an exception occurs.

View File

@@ -456,7 +456,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) {
return cache.retrieve(key, () -> (CompletableFuture<?>) invokeOperation(invoker));
return doRetrieve(cache, key, () -> (CompletableFuture<?>) invokeOperation(invoker));
}
if (this.reactiveCachingHandler != null) {
Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key);
@@ -465,7 +465,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
}
}
try {
return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
return wrapCacheValue(method, doGet(cache, key, () -> unwrapReturnValue(invokeOperation(invoker))));
}
catch (Cache.ValueRetrievalException ex) {
// Directly propagate ThrowableWrapper from the invoker,
@@ -515,7 +515,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
for (Cache cache : context.getCaches()) {
if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) {
CompletableFuture<?> result = cache.retrieve(key);
CompletableFuture<?> result = doRetrieve(cache, key);
if (result != null) {
return result.exceptionally(ex -> {
getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key);
@@ -1144,7 +1144,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
ReactiveAdapter adapter = this.registry.getAdapter(context.getMethod().getReturnType());
if (adapter != null) {
CompletableFuture<?> cachedFuture = cache.retrieve(key);
CompletableFuture<?> cachedFuture = doRetrieve(cache, key);
if (cachedFuture == null) {
return null;
}

View File

@@ -17,11 +17,15 @@
package org.springframework.cache.interceptor;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
@@ -39,6 +43,8 @@ import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.BDDMockito.willThrow;
@@ -83,11 +89,56 @@ class CacheErrorHandlerTests {
willThrow(exception).given(this.cache).get(0L);
Object result = this.simpleService.get(0L);
verify(this.errorHandler).handleCacheGetError(exception, cache, 0L);
verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L);
verify(this.cache).get(0L);
verify(this.cache).put(0L, result); // result of the invocation
}
@Test
public void getSyncFail() {
UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get");
willThrow(exception).given(this.cache).get(eq(0L), any(Callable.class));
Object result = this.simpleService.getSync(0L);
assertThat(result).isEqualTo(0L);
verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L);
verify(this.cache).get(eq(0L), any(Callable.class));
}
@Test
public void getCompletableFutureFail() {
UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get");
willThrow(exception).given(this.cache).retrieve(eq(0L));
Object result = this.simpleService.getFuture(0L).join();
assertThat(result).isEqualTo(0L);
verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L);
verify(this.cache).retrieve(eq(0L));
}
@Test
public void getMonoFail() {
UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get");
willThrow(exception).given(this.cache).retrieve(eq(0L));
Object result = this.simpleService.getMono(0L).block();
assertThat(result).isEqualTo(0L);
verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L);
verify(this.cache).retrieve(eq(0L));
}
@Test
public void getFluxFail() {
UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get");
willThrow(exception).given(this.cache).retrieve(eq(0L));
Object result = this.simpleService.getFlux(0L).blockLast();
assertThat(result).isEqualTo(0L);
verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L);
verify(this.cache).retrieve(eq(0L));
}
@Test
void getAndPutFail() {
UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get");
@@ -220,6 +271,26 @@ class CacheErrorHandlerTests {
return this.counter.getAndIncrement();
}
@Cacheable(sync = true)
public Object getSync(long id) {
return this.counter.getAndIncrement();
}
@Cacheable
public CompletableFuture<Long> getFuture(long id) {
return CompletableFuture.completedFuture(this.counter.getAndIncrement());
}
@Cacheable
public Mono<Long> getMono(long id) {
return Mono.just(this.counter.getAndIncrement());
}
@Cacheable
public Flux<Long> getFlux(long id) {
return Flux.just(this.counter.getAndIncrement(), 0L);
}
@CachePut
public Object put(long id) {
return this.counter.getAndIncrement();