Skip to content

Commit 1410c46

Browse files
committed
Support for late-determined cache misses from retrieve(key)
Closes gh-31637
1 parent 8ffbecc commit 1410c46

File tree

6 files changed

+411
-119
lines changed

6 files changed

+411
-119
lines changed

spring-context-support/spring-context-support.gradle

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ dependencies {
66
api(project(":spring-core"))
77
optional(project(":spring-jdbc")) // for Quartz support
88
optional(project(":spring-tx")) // for Quartz support
9+
optional("com.github.ben-manes.caffeine:caffeine")
910
optional("jakarta.activation:jakarta.activation-api")
1011
optional("jakarta.mail:jakarta.mail-api")
1112
optional("javax.cache:cache-api")
12-
optional("com.github.ben-manes.caffeine:caffeine")
13-
optional("org.quartz-scheduler:quartz")
1413
optional("org.freemarker:freemarker")
14+
optional("org.quartz-scheduler:quartz")
1515
testFixturesApi("org.junit.jupiter:junit-jupiter-api")
1616
testFixturesImplementation("org.assertj:assertj-core")
1717
testFixturesImplementation("org.mockito:mockito-core")
@@ -20,10 +20,11 @@ dependencies {
2020
testImplementation(testFixtures(project(":spring-context")))
2121
testImplementation(testFixtures(project(":spring-core")))
2222
testImplementation(testFixtures(project(":spring-tx")))
23-
testImplementation("org.hsqldb:hsqldb")
23+
testImplementation("io.projectreactor:reactor-core")
2424
testImplementation("jakarta.annotation:jakarta.annotation-api")
25-
testRuntimeOnly("org.ehcache:jcache")
25+
testImplementation("org.hsqldb:hsqldb")
26+
testRuntimeOnly("com.sun.mail:jakarta.mail")
2627
testRuntimeOnly("org.ehcache:ehcache")
28+
testRuntimeOnly("org.ehcache:jcache")
2729
testRuntimeOnly("org.glassfish:jakarta.el")
28-
testRuntimeOnly("com.sun.mail:jakarta.mail")
2930
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cache.caffeine;
18+
19+
import java.util.List;
20+
import java.util.concurrent.CompletableFuture;
21+
import java.util.concurrent.atomic.AtomicLong;
22+
23+
import org.junit.jupiter.api.Test;
24+
import reactor.core.publisher.Flux;
25+
import reactor.core.publisher.Mono;
26+
27+
import org.springframework.cache.CacheManager;
28+
import org.springframework.cache.annotation.CacheConfig;
29+
import org.springframework.cache.annotation.Cacheable;
30+
import org.springframework.cache.annotation.EnableCaching;
31+
import org.springframework.context.ApplicationContext;
32+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.context.annotation.Configuration;
35+
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
38+
/**
39+
* Tests for annotation-based caching methods that use reactive operators.
40+
*
41+
* @author Juergen Hoeller
42+
* @since 6.1
43+
*/
44+
public class CaffeineReactiveCachingTests {
45+
46+
@Test
47+
void withCaffeineAsyncCache() {
48+
ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class, ReactiveCacheableService.class);
49+
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
50+
51+
Object key = new Object();
52+
53+
Long r1 = service.cacheFuture(key).join();
54+
Long r2 = service.cacheFuture(key).join();
55+
Long r3 = service.cacheFuture(key).join();
56+
57+
assertThat(r1).isNotNull();
58+
assertThat(r1).isSameAs(r2).isSameAs(r3);
59+
60+
key = new Object();
61+
62+
r1 = service.cacheMono(key).block();
63+
r2 = service.cacheMono(key).block();
64+
r3 = service.cacheMono(key).block();
65+
66+
assertThat(r1).isNotNull();
67+
assertThat(r1).isSameAs(r2).isSameAs(r3);
68+
69+
key = new Object();
70+
71+
r1 = service.cacheFlux(key).blockFirst();
72+
r2 = service.cacheFlux(key).blockFirst();
73+
r3 = service.cacheFlux(key).blockFirst();
74+
75+
assertThat(r1).isNotNull();
76+
assertThat(r1).isSameAs(r2).isSameAs(r3);
77+
78+
key = new Object();
79+
80+
List<Long> l1 = service.cacheFlux(key).collectList().block();
81+
List<Long> l2 = service.cacheFlux(key).collectList().block();
82+
List<Long> l3 = service.cacheFlux(key).collectList().block();
83+
84+
assertThat(l1).isNotNull();
85+
assertThat(l1).isEqualTo(l2).isEqualTo(l3);
86+
87+
key = new Object();
88+
89+
r1 = service.cacheMono(key).block();
90+
r2 = service.cacheMono(key).block();
91+
r3 = service.cacheMono(key).block();
92+
93+
assertThat(r1).isNotNull();
94+
assertThat(r1).isSameAs(r2).isSameAs(r3);
95+
96+
// Same key as for Mono, reusing its cached value
97+
98+
r1 = service.cacheFlux(key).blockFirst();
99+
r2 = service.cacheFlux(key).blockFirst();
100+
r3 = service.cacheFlux(key).blockFirst();
101+
102+
assertThat(r1).isNotNull();
103+
assertThat(r1).isSameAs(r2).isSameAs(r3);
104+
}
105+
106+
107+
@CacheConfig(cacheNames = "first")
108+
static class ReactiveCacheableService {
109+
110+
private final AtomicLong counter = new AtomicLong();
111+
112+
@Cacheable
113+
CompletableFuture<Long> cacheFuture(Object arg) {
114+
return CompletableFuture.completedFuture(this.counter.getAndIncrement());
115+
}
116+
117+
@Cacheable
118+
Mono<Long> cacheMono(Object arg) {
119+
return Mono.just(this.counter.getAndIncrement());
120+
}
121+
122+
@Cacheable
123+
Flux<Long> cacheFlux(Object arg) {
124+
return Flux.just(this.counter.getAndIncrement(), 0L);
125+
}
126+
}
127+
128+
129+
@Configuration(proxyBeanMethods = false)
130+
@EnableCaching
131+
static class Config {
132+
133+
@Bean
134+
CacheManager cacheManager() {
135+
CaffeineCacheManager ccm = new CaffeineCacheManager("first");
136+
ccm.setAsyncCacheMode(true);
137+
return ccm;
138+
}
139+
}
140+
141+
}

spring-context/src/main/java/org/springframework/cache/Cache.java

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
/**
2626
* Interface that defines common cache operations.
2727
*
28-
* <p>Serves as an SPI for Spring's annotation-based caching model
29-
* ({@link org.springframework.cache.annotation.Cacheable} and co)
30-
* as well as an API for direct usage in applications.
28+
* <p>Serves primarily as an SPI for Spring's annotation-based caching
29+
* model ({@link org.springframework.cache.annotation.Cacheable} and co)
30+
* and secondarily as an API for direct usage in applications.
3131
*
3232
* <p><b>Note:</b> Due to the generic use of caching, it is recommended
3333
* that implementations allow storage of {@code null} values
@@ -113,16 +113,26 @@ public interface Cache {
113113
* wrapped in a {@link CompletableFuture}. This operation must not block
114114
* but is allowed to return a completed {@link CompletableFuture} if the
115115
* corresponding value is immediately available.
116-
* <p>Returns {@code null} if the cache contains no mapping for this key;
117-
* otherwise, the cached value (which may be {@code null} itself) will
118-
* be returned in the {@link CompletableFuture}.
116+
* <p>Can return {@code null} if the cache can immediately determine that
117+
* it contains no mapping for this key (e.g. through an in-memory key map).
118+
* Otherwise, the cached value will be returned in the {@link CompletableFuture},
119+
* with {@code null} indicating a late-determined cache miss (and a nested
120+
* {@link ValueWrapper} potentially indicating a nullable cached value).
119121
* @param key the key whose associated value is to be returned
120-
* @return the value to which this cache maps the specified key,
121-
* contained within a {@link CompletableFuture} which may also hold
122-
* a cached {@code null} value. A straight {@code null} being
123-
* returned means that the cache contains no mapping for this key.
122+
* @return the value to which this cache maps the specified key, contained
123+
* within a {@link CompletableFuture} which may also be empty when a cache
124+
* miss has been late-determined. A straight {@code null} being returned
125+
* means that the cache immediately determined that it contains no mapping
126+
* for this key. A {@link ValueWrapper} contained within the
127+
* {@code CompletableFuture} can indicate a cached value that is potentially
128+
* {@code null}; this is sensible in a late-determined scenario where a regular
129+
* CompletableFuture-contained {@code null} indicates a cache miss. However,
130+
* an early-determined cache will usually return the plain cached value here,
131+
* and a late-determined cache may also return a plain value if it does not
132+
* support the actual caching of {@code null} values. Spring's common cache
133+
* processing can deal with all variants of these implementation strategies.
124134
* @since 6.1
125-
* @see #get(Object)
135+
* @see #retrieve(Object, Supplier)
126136
*/
127137
@Nullable
128138
default CompletableFuture<?> retrieve(Object key) {

0 commit comments

Comments
 (0)