Support for late-determined cache misses from retrieve(key)

Closes gh-31637
This commit is contained in:
Juergen Hoeller
2023-11-21 23:11:34 +01:00
parent 8ffbecc7c3
commit 1410c466b7
6 changed files with 413 additions and 121 deletions

View File

@@ -182,11 +182,13 @@ class CacheReproTests {
Cache cache = context.getBean(CacheManager.class).getCache("itemCache");
TestBean tb = bean.findById("tb1").join();
assertThat(tb).isNotNull();
assertThat(bean.findById("tb1").join()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
bean.clear().join();
TestBean tb2 = bean.findById("tb1").join();
assertThat(tb2).isNotNull();
assertThat(tb2).isNotSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb2);
@@ -230,11 +232,13 @@ class CacheReproTests {
Cache cache = context.getBean(CacheManager.class).getCache("itemCache");
TestBean tb = bean.findById("tb1").block();
assertThat(tb).isNotNull();
assertThat(bean.findById("tb1").block()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
bean.clear().block();
TestBean tb2 = bean.findById("tb1").block();
assertThat(tb2).isNotNull();
assertThat(tb2).isNotSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb2);
@@ -278,11 +282,13 @@ class CacheReproTests {
Cache cache = context.getBean(CacheManager.class).getCache("itemCache");
List<TestBean> tb = bean.findById("tb1").collectList().block();
assertThat(tb).isNotEmpty();
assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb);
assertThat(cache.get("tb1").get()).isEqualTo(tb);
bean.clear().blockLast();
List<TestBean> tb2 = bean.findById("tb1").collectList().block();
assertThat(tb2).isNotEmpty();
assertThat(tb2).isNotEqualTo(tb);
assertThat(cache.get("tb1").get()).isEqualTo(tb2);

View File

@@ -16,14 +16,21 @@
package org.springframework.cache.annotation;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -34,24 +41,71 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for annotation-based caching methods that use reactive operators.
*
* @author Stephane Nicoll
* @author Juergen Hoeller
* @since 6.1
*/
public class ReactiveCachingTests {
private final ConfigurableApplicationContext ctx;
@ParameterizedTest
@ValueSource(classes = {EarlyCacheHitDeterminationConfig.class,
LateCacheHitDeterminationConfig.class,
LateCacheHitDeterminationWithValueWrapperConfig.class})
void cacheHitDetermination(Class<?> configClass) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
private final ReactiveCacheableService service;
public ReactiveCachingTests() {
this.ctx = new AnnotationConfigApplicationContext(TestConfig.class);
this.service = this.ctx.getBean(ReactiveCacheableService.class);
}
@Test
void cache() {
Object key = new Object();
Long r1 = this.service.cache(key).block();
Long r2 = this.service.cache(key).block();
Long r3 = this.service.cache(key).block();
Long r1 = service.cacheFuture(key).join();
Long r2 = service.cacheFuture(key).join();
Long r3 = service.cacheFuture(key).join();
assertThat(r1).isNotNull();
assertThat(r1).isSameAs(r2).isSameAs(r3);
key = new Object();
r1 = service.cacheMono(key).block();
r2 = service.cacheMono(key).block();
r3 = service.cacheMono(key).block();
assertThat(r1).isNotNull();
assertThat(r1).isSameAs(r2).isSameAs(r3);
key = new Object();
r1 = service.cacheFlux(key).blockFirst();
r2 = service.cacheFlux(key).blockFirst();
r3 = service.cacheFlux(key).blockFirst();
assertThat(r1).isNotNull();
assertThat(r1).isSameAs(r2).isSameAs(r3);
key = new Object();
List<Long> l1 = service.cacheFlux(key).collectList().block();
List<Long> l2 = service.cacheFlux(key).collectList().block();
List<Long> l3 = service.cacheFlux(key).collectList().block();
assertThat(l1).isNotNull();
assertThat(l1).isEqualTo(l2).isEqualTo(l3);
key = new Object();
r1 = service.cacheMono(key).block();
r2 = service.cacheMono(key).block();
r3 = service.cacheMono(key).block();
assertThat(r1).isNotNull();
assertThat(r1).isSameAs(r2).isSameAs(r3);
// Same key as for Mono, reusing its cached value
r1 = service.cacheFlux(key).blockFirst();
r2 = service.cacheFlux(key).blockFirst();
r3 = service.cacheFlux(key).blockFirst();
assertThat(r1).isNotNull();
assertThat(r1).isSameAs(r2).isSameAs(r3);
}
@@ -62,27 +116,78 @@ public class ReactiveCachingTests {
private final AtomicLong counter = new AtomicLong();
@Cacheable
Mono<Long> cache(Object arg1) {
CompletableFuture<Long> cacheFuture(Object arg) {
return CompletableFuture.completedFuture(this.counter.getAndIncrement());
}
@Cacheable
Mono<Long> cacheMono(Object arg) {
return Mono.just(this.counter.getAndIncrement());
}
@Cacheable
Flux<Long> cacheFlux(Object arg) {
return Flux.just(this.counter.getAndIncrement(), 0L);
}
}
@Configuration(proxyBeanMethods = false)
@EnableCaching
static class TestConfig {
static class EarlyCacheHitDeterminationConfig {
@Bean
CacheManager cacheManager() {
return new ConcurrentMapCacheManager("first");
}
}
@Configuration(proxyBeanMethods = false)
@EnableCaching
static class LateCacheHitDeterminationConfig {
@Bean
ReactiveCacheableService reactiveCacheableService() {
return new ReactiveCacheableService();
CacheManager cacheManager() {
return new ConcurrentMapCacheManager("first") {
@Override
protected Cache createConcurrentMapCache(String name) {
return new ConcurrentMapCache(name, isAllowNullValues()) {
@Override
public CompletableFuture<?> retrieve(Object key) {
return CompletableFuture.completedFuture(lookup(key));
}
};
}
};
}
}
@Configuration(proxyBeanMethods = false)
@EnableCaching
static class LateCacheHitDeterminationWithValueWrapperConfig {
@Bean
CacheManager cacheManager() {
return new ConcurrentMapCacheManager("first") {
@Override
protected Cache createConcurrentMapCache(String name) {
return new ConcurrentMapCache(name, isAllowNullValues()) {
@Override
public CompletableFuture<?> retrieve(Object key) {
Object value = lookup(key);
if (value != null) {
return CompletableFuture.completedFuture(new SimpleValueWrapper(fromStoreValue(value)));
}
else {
return CompletableFuture.completedFuture(null);
}
}
};
}
};
}
}
}