Browse Source

Consistent CacheErrorHandler processing for @Cacheable(sync=true)

Closes gh-34708
pull/34732/head
Juergen Hoeller 9 months ago
parent
commit
4e5979c75a
  1. 11
      spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java
  2. 106
      spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java
  3. 253
      spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java

11
spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java vendored

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -105,7 +105,7 @@ public abstract class AbstractCacheInvoker {
return valueLoader.call(); return valueLoader.call();
} }
catch (Exception ex2) { catch (Exception ex2) {
throw new RuntimeException(ex2); throw new Cache.ValueRetrievalException(key, valueLoader, ex);
} }
} }
} }
@ -124,16 +124,12 @@ public abstract class AbstractCacheInvoker {
try { try {
return cache.retrieve(key); return cache.retrieve(key);
} }
catch (Cache.ValueRetrievalException ex) {
throw ex;
}
catch (RuntimeException ex) { catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key); getErrorHandler().handleCacheGetError(ex, cache, key);
return null; return null;
} }
} }
/** /**
* Execute {@link Cache#retrieve(Object, Supplier)} on the specified * Execute {@link Cache#retrieve(Object, Supplier)} on the specified
* {@link Cache} and invoke the error handler if an exception occurs. * {@link Cache} and invoke the error handler if an exception occurs.
@ -146,9 +142,6 @@ public abstract class AbstractCacheInvoker {
try { try {
return cache.retrieve(key, valueLoader); return cache.retrieve(key, valueLoader);
} }
catch (Cache.ValueRetrievalException ex) {
throw ex;
}
catch (RuntimeException ex) { catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key); getErrorHandler().handleCacheGetError(ex, cache, key);
return valueLoader.get(); return valueLoader.get();

106
spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java vendored

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,6 +26,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -449,6 +450,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
return cacheHit; return cacheHit;
} }
@SuppressWarnings("unchecked")
@Nullable @Nullable
private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
@ -456,7 +458,33 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next(); Cache cache = context.getCaches().iterator().next();
if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) { if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) {
return doRetrieve(cache, key, () -> (CompletableFuture<?>) invokeOperation(invoker)); AtomicBoolean invokeFailure = new AtomicBoolean(false);
CompletableFuture<?> result = doRetrieve(cache, key,
() -> {
CompletableFuture<?> invokeResult = ((CompletableFuture<?>) invokeOperation(invoker));
if (invokeResult == null) {
return null;
}
return invokeResult.exceptionallyCompose(ex -> {
invokeFailure.set(true);
return CompletableFuture.failedFuture(ex);
});
});
return result.exceptionallyCompose(ex -> {
if (!(ex instanceof RuntimeException rex)) {
return CompletableFuture.failedFuture(ex);
}
try {
getErrorHandler().handleCacheGetError(rex, cache, key);
if (invokeFailure.get()) {
return CompletableFuture.failedFuture(ex);
}
return (CompletableFuture) invokeOperation(invoker);
}
catch (Throwable ex2) {
return CompletableFuture.failedFuture(ex2);
}
});
} }
if (this.reactiveCachingHandler != null) { if (this.reactiveCachingHandler != null) {
Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key); Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key);
@ -517,9 +545,17 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) { if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) {
CompletableFuture<?> result = doRetrieve(cache, key); CompletableFuture<?> result = doRetrieve(cache, key);
if (result != null) { if (result != null) {
return result.exceptionally(ex -> { return result.exceptionallyCompose(ex -> {
getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); if (!(ex instanceof RuntimeException rex)) {
return null; return CompletableFuture.failedFuture(ex);
}
try {
getErrorHandler().handleCacheGetError(rex, cache, key);
return CompletableFuture.completedFuture(null);
}
catch (Throwable ex2) {
return CompletableFuture.failedFuture(ex2);
}
}).thenCompose(value -> (CompletableFuture<?>) evaluate( }).thenCompose(value -> (CompletableFuture<?>) evaluate(
(value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null), (value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null),
invoker, method, contexts)); invoker, method, contexts));
@ -1097,32 +1133,72 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
@SuppressWarnings({"rawtypes", "unchecked"})
@Nullable @Nullable
public Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) { public Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) {
AtomicBoolean invokeFailure = new AtomicBoolean(false);
ReactiveAdapter adapter = this.registry.getAdapter(method.getReturnType()); ReactiveAdapter adapter = this.registry.getAdapter(method.getReturnType());
if (adapter != null) { if (adapter != null) {
if (adapter.isMultiValue()) { if (adapter.isMultiValue()) {
// Flux or similar // Flux or similar
return adapter.fromPublisher(Flux.from(Mono.fromFuture( return adapter.fromPublisher(Flux.from(Mono.fromFuture(
cache.retrieve(key, doRetrieve(cache, key,
() -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().toFuture()))) () -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().doOnError(ex -> invokeFailure.set(true)).toFuture())))
.flatMap(Flux::fromIterable)); .flatMap(Flux::fromIterable)
.onErrorResume(RuntimeException.class, ex -> {
try {
getErrorHandler().handleCacheGetError(ex, cache, key);
if (invokeFailure.get()) {
return Flux.error(ex);
}
return Flux.from(adapter.toPublisher(invokeOperation(invoker)));
}
catch (RuntimeException exception) {
return Flux.error(exception);
}
}));
} }
else { else {
// Mono or similar // Mono or similar
return adapter.fromPublisher(Mono.fromFuture( return adapter.fromPublisher(Mono.fromFuture(
cache.retrieve(key, doRetrieve(cache, key,
() -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).toFuture()))); () -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).doOnError(ex -> invokeFailure.set(true)).toFuture()))
.onErrorResume(RuntimeException.class, ex -> {
try {
getErrorHandler().handleCacheGetError(ex, cache, key);
if (invokeFailure.get()) {
return Mono.error(ex);
}
return Mono.from(adapter.toPublisher(invokeOperation(invoker)));
}
catch (RuntimeException exception) {
return Mono.error(exception);
}
}));
} }
} }
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) { if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) {
return Mono.fromFuture(cache.retrieve(key, () -> { return Mono.fromFuture(doRetrieve(cache, key, () -> {
Mono<?> mono = ((Mono<?>) invokeOperation(invoker)); Mono<?> mono = (Mono<?>) invokeOperation(invoker);
if (mono == null) { if (mono != null) {
mono = mono.doOnError(ex -> invokeFailure.set(true));
}
else {
mono = Mono.empty(); mono = Mono.empty();
} }
return mono.toFuture(); return mono.toFuture();
})); })).onErrorResume(RuntimeException.class, ex -> {
try {
getErrorHandler().handleCacheGetError(ex, cache, key);
if (invokeFailure.get()) {
return Mono.error(ex);
}
return (Mono) invokeOperation(invoker);
}
catch (RuntimeException exception) {
return Mono.error(exception);
}
});
} }
return NOT_HANDLED; return NOT_HANDLED;
} }
@ -1137,7 +1213,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
return NOT_HANDLED; return NOT_HANDLED;
} }
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings({"rawtypes", "unchecked"})
@Nullable @Nullable
public Object findInCaches(CacheOperationContext context, Cache cache, Object key, public Object findInCaches(CacheOperationContext context, Cache cache, Object key,
CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {

253
spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java vendored

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,7 +19,9 @@ package org.springframework.cache.annotation;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@ -40,6 +42,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable; import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;
/** /**
@ -58,8 +61,8 @@ class ReactiveCachingTests {
LateCacheHitDeterminationWithValueWrapperConfig.class}) LateCacheHitDeterminationWithValueWrapperConfig.class})
void cacheHitDetermination(Class<?> configClass) { void cacheHitDetermination(Class<?> configClass) {
AnnotationConfigApplicationContext ctx = AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); configClass, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Object key = new Object(); Object key = new Object();
@ -119,58 +122,58 @@ class ReactiveCachingTests {
ctx.close(); ctx.close();
} }
@Test @ParameterizedTest
void cacheErrorHandlerWithLoggingCacheErrorHandler() { @ValueSource(classes = {EarlyCacheHitDeterminationConfig.class,
AnnotationConfigApplicationContext ctx = EarlyCacheHitDeterminationWithoutNullValuesConfig.class,
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class); LateCacheHitDeterminationConfig.class,
LateCacheHitDeterminationWithValueWrapperConfig.class})
void fluxCacheDoesntDependOnFirstRequest(Class<?> configClass) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
configClass, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Object key = new Object(); Object key = new Object();
Long r1 = service.cacheFuture(key).join();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFuture").isEqualTo(0L);
key = new Object(); List<Long> l1 = service.cacheFlux(key).take(1L, true).collectList().block();
List<Long> l2 = service.cacheFlux(key).take(3L, true).collectList().block();
r1 = service.cacheMono(key).block(); List<Long> l3 = service.cacheFlux(key).collectList().block();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheMono").isEqualTo(1L);
key = new Object(); Long first = l1.get(0);
r1 = service.cacheFlux(key).blockFirst(); assertThat(l1).as("l1").containsExactly(first);
assertThat(l2).as("l2").containsExactly(first, 0L, -1L);
assertThat(l3).as("l3").containsExactly(first, 0L, -1L, -2L, -3L);
assertThat(r1).isNotNull(); ctx.close();
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
} }
@Test @Test
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndMethodError() { void cacheErrorHandlerWithSimpleCacheErrorHandler() {
AnnotationConfigApplicationContext ctx = AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); ExceptionCacheManager.class, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Object key = new Object(); Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join());
StepVerifier.create(service.cacheMono(key)) assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class)
.expectErrorMessage("mono service error") .extracting(Throwable::getCause)
.verify(); .isInstanceOf(UnsupportedOperationException.class);
key = new Object(); Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block());
StepVerifier.create(service.cacheFlux(key)) assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class);
.expectErrorMessage("flux service error")
.verify(); Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst());
assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class);
} }
@Test @Test
void cacheErrorHandlerWithSimpleCacheErrorHandler() { void cacheErrorHandlerWithSimpleCacheErrorHandlerAndSync() {
AnnotationConfigApplicationContext ctx = AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class); ExceptionCacheManager.class, ReactiveSyncCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class);
Throwable completableFuturThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join());
assertThat(completableFuturThrowable).isInstanceOf(CompletionException.class) assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class)
.extracting(Throwable::getCause) .extracting(Throwable::getCause)
.isInstanceOf(UnsupportedOperationException.class); .isInstanceOf(UnsupportedOperationException.class);
@ -181,32 +184,81 @@ class ReactiveCachingTests {
assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class); assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class);
} }
@ParameterizedTest @Test
@ValueSource(classes = {EarlyCacheHitDeterminationConfig.class, void cacheErrorHandlerWithLoggingCacheErrorHandler() {
EarlyCacheHitDeterminationWithoutNullValuesConfig.class, AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
LateCacheHitDeterminationConfig.class, ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class);
LateCacheHitDeterminationWithValueWrapperConfig.class}) ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
void fluxCacheDoesntDependOnFirstRequest(Class<?> configClass) {
Long r1 = service.cacheFuture(new Object()).join();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFuture").isEqualTo(0L);
r1 = service.cacheMono(new Object()).block();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheMono").isEqualTo(1L);
r1 = service.cacheFlux(new Object()).blockFirst();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
}
@Test
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndSync() {
AnnotationConfigApplicationContext ctx = AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveSyncCacheableService.class, ErrorHandlerCachingConfiguration.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class);
Object key = new Object(); Long r1 = service.cacheFuture(new Object()).join();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFuture").isEqualTo(0L);
List<Long> l1 = service.cacheFlux(key).take(1L, true).collectList().block(); r1 = service.cacheMono(new Object()).block();
List<Long> l2 = service.cacheFlux(key).take(3L, true).collectList().block(); assertThat(r1).isNotNull();
List<Long> l3 = service.cacheFlux(key).collectList().block(); assertThat(r1).as("cacheMono").isEqualTo(1L);
Long first = l1.get(0); r1 = service.cacheFlux(new Object()).blockFirst();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
}
assertThat(l1).as("l1").containsExactly(first); @Test
assertThat(l2).as("l2").containsExactly(first, 0L, -1L); void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationException() {
assertThat(l3).as("l3").containsExactly(first, 0L, -1L, -2L, -3L); AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class);
ReactiveFailureCacheableService service = ctx.getBean(ReactiveFailureCacheableService.class);
ctx.close(); assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join())
.withMessage(IllegalStateException.class.getName() + ": future service error");
StepVerifier.create(service.cacheMono(new Object()))
.expectErrorMessage("mono service error")
.verify();
StepVerifier.create(service.cacheFlux(new Object()))
.expectErrorMessage("flux service error")
.verify();
} }
@Test
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationExceptionAndSync() {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveSyncFailureCacheableService.class, ErrorHandlerCachingConfiguration.class);
ReactiveSyncFailureCacheableService service = ctx.getBean(ReactiveSyncFailureCacheableService.class);
assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join())
.withMessage(IllegalStateException.class.getName() + ": future service error");
StepVerifier.create(service.cacheMono(new Object()))
.expectErrorMessage("mono service error")
.verify();
StepVerifier.create(service.cacheFlux(new Object()))
.expectErrorMessage("flux service error")
.verify();
}
@CacheConfig(cacheNames = "first") @CacheConfig(cacheNames = "first")
static class ReactiveCacheableService { static class ReactiveCacheableService {
@ -232,16 +284,94 @@ class ReactiveCachingTests {
} }
} }
@CacheConfig(cacheNames = "first")
static class ReactiveSyncCacheableService {
private final AtomicLong counter = new AtomicLong();
@Cacheable(sync = true)
CompletableFuture<Long> cacheFuture(Object arg) {
return CompletableFuture.completedFuture(this.counter.getAndIncrement());
}
@Cacheable(sync = true)
Mono<Long> cacheMono(Object arg) {
return Mono.defer(() -> Mono.just(this.counter.getAndIncrement()));
}
@Cacheable(sync = true)
Flux<Long> cacheFlux(Object arg) {
return Flux.defer(() -> Flux.just(this.counter.getAndIncrement(), 0L, -1L, -2L, -3L));
}
}
@CacheConfig(cacheNames = "first") @CacheConfig(cacheNames = "first")
static class ReactiveFailureCacheableService extends ReactiveCacheableService { static class ReactiveFailureCacheableService {
private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean();
private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean();
private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean();
@Cacheable
CompletableFuture<Long> cacheFuture(Object arg) {
if (!this.cacheFutureInvoked.compareAndSet(false, true)) {
return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice"));
}
return CompletableFuture.failedFuture(new IllegalStateException("future service error"));
}
@Cacheable @Cacheable
Mono<Long> cacheMono(Object arg) { Mono<Long> cacheMono(Object arg) {
if (!this.cacheMonoInvoked.compareAndSet(false, true)) {
return Mono.error(new IllegalStateException("mono service invoked twice"));
}
return Mono.error(new IllegalStateException("mono service error")); return Mono.error(new IllegalStateException("mono service error"));
} }
@Cacheable @Cacheable
Flux<Long> cacheFlux(Object arg) { Flux<Long> cacheFlux(Object arg) {
if (!this.cacheFluxInvoked.compareAndSet(false, true)) {
return Flux.error(new IllegalStateException("flux service invoked twice"));
}
return Flux.error(new IllegalStateException("flux service error"));
}
}
@CacheConfig(cacheNames = "first")
static class ReactiveSyncFailureCacheableService {
private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean();
private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean();
private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean();
@Cacheable(sync = true)
CompletableFuture<Long> cacheFuture(Object arg) {
if (!this.cacheFutureInvoked.compareAndSet(false, true)) {
return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice"));
}
return CompletableFuture.failedFuture(new IllegalStateException("future service error"));
}
@Cacheable(sync = true)
Mono<Long> cacheMono(Object arg) {
if (!this.cacheMonoInvoked.compareAndSet(false, true)) {
return Mono.error(new IllegalStateException("mono service invoked twice"));
}
return Mono.error(new IllegalStateException("mono service error"));
}
@Cacheable(sync = true)
Flux<Long> cacheFlux(Object arg) {
if (!this.cacheFluxInvoked.compareAndSet(false, true)) {
return Flux.error(new IllegalStateException("flux service invoked twice"));
}
return Flux.error(new IllegalStateException("flux service error")); return Flux.error(new IllegalStateException("flux service error"));
} }
} }
@ -323,6 +453,7 @@ class ReactiveCachingTests {
} }
} }
@Configuration @Configuration
static class ErrorHandlerCachingConfiguration implements CachingConfigurer { static class ErrorHandlerCachingConfiguration implements CachingConfigurer {
@ -333,6 +464,7 @@ class ReactiveCachingTests {
} }
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableCaching @EnableCaching
static class ExceptionCacheManager { static class ExceptionCacheManager {
@ -345,11 +477,12 @@ class ReactiveCachingTests {
return new ConcurrentMapCache(name, isAllowNullValues()) { return new ConcurrentMapCache(name, isAllowNullValues()) {
@Override @Override
public CompletableFuture<?> retrieve(Object key) { public CompletableFuture<?> retrieve(Object key) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve"));
throw new UnsupportedOperationException("Test exception on retrieve"); }
}); @Override
public <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve"));
} }
@Override @Override
public void put(Object key, @Nullable Object value) { public void put(Object key, @Nullable Object value) {
throw new UnsupportedOperationException("Test exception on put"); throw new UnsupportedOperationException("Test exception on put");

Loading…
Cancel
Save