Skip to content

Commit

Permalink
Support for late-determined cache misses from retrieve(key)
Browse files Browse the repository at this point in the history
Closes gh-31637
  • Loading branch information
jhoeller committed Nov 21, 2023
1 parent 8ffbecc commit 1410c46
Show file tree
Hide file tree
Showing 6 changed files with 411 additions and 119 deletions.
11 changes: 6 additions & 5 deletions spring-context-support/spring-context-support.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ dependencies {
api(project(":spring-core"))
optional(project(":spring-jdbc")) // for Quartz support
optional(project(":spring-tx")) // for Quartz support
optional("com.github.ben-manes.caffeine:caffeine")
optional("jakarta.activation:jakarta.activation-api")
optional("jakarta.mail:jakarta.mail-api")
optional("javax.cache:cache-api")
optional("com.github.ben-manes.caffeine:caffeine")
optional("org.quartz-scheduler:quartz")
optional("org.freemarker:freemarker")
optional("org.quartz-scheduler:quartz")
testFixturesApi("org.junit.jupiter:junit-jupiter-api")
testFixturesImplementation("org.assertj:assertj-core")
testFixturesImplementation("org.mockito:mockito-core")
Expand All @@ -20,10 +20,11 @@ dependencies {
testImplementation(testFixtures(project(":spring-context")))
testImplementation(testFixtures(project(":spring-core")))
testImplementation(testFixtures(project(":spring-tx")))
testImplementation("org.hsqldb:hsqldb")
testImplementation("io.projectreactor:reactor-core")
testImplementation("jakarta.annotation:jakarta.annotation-api")
testRuntimeOnly("org.ehcache:jcache")
testImplementation("org.hsqldb:hsqldb")
testRuntimeOnly("com.sun.mail:jakarta.mail")
testRuntimeOnly("org.ehcache:ehcache")
testRuntimeOnly("org.ehcache:jcache")
testRuntimeOnly("org.glassfish:jakarta.el")
testRuntimeOnly("com.sun.mail:jakarta.mail")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cache.caffeine;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;

import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for annotation-based caching methods that use reactive operators.
*
* @author Juergen Hoeller
* @since 6.1
*/
public class CaffeineReactiveCachingTests {

@Test
void withCaffeineAsyncCache() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);

Object key = new Object();

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);
}


@CacheConfig(cacheNames = "first")
static class ReactiveCacheableService {

private final AtomicLong counter = new AtomicLong();

@Cacheable
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 Config {

@Bean
CacheManager cacheManager() {
CaffeineCacheManager ccm = new CaffeineCacheManager("first");
ccm.setAsyncCacheMode(true);
return ccm;
}
}

}
32 changes: 21 additions & 11 deletions spring-context/src/main/java/org/springframework/cache/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
/**
* Interface that defines common cache operations.
*
* <p>Serves as an SPI for Spring's annotation-based caching model
* ({@link org.springframework.cache.annotation.Cacheable} and co)
* as well as an API for direct usage in applications.
* <p>Serves primarily as an SPI for Spring's annotation-based caching
* model ({@link org.springframework.cache.annotation.Cacheable} and co)
* and secondarily as an API for direct usage in applications.
*
* <p><b>Note:</b> Due to the generic use of caching, it is recommended
* that implementations allow storage of {@code null} values
Expand Down Expand Up @@ -113,16 +113,26 @@ public interface Cache {
* wrapped in a {@link CompletableFuture}. This operation must not block
* but is allowed to return a completed {@link CompletableFuture} if the
* corresponding value is immediately available.
* <p>Returns {@code null} if the cache contains no mapping for this key;
* otherwise, the cached value (which may be {@code null} itself) will
* be returned in the {@link CompletableFuture}.
* <p>Can return {@code null} if the cache can immediately determine that
* it contains no mapping for this key (e.g. through an in-memory key map).
* Otherwise, the cached value will be returned in the {@link CompletableFuture},
* with {@code null} indicating a late-determined cache miss (and a nested
* {@link ValueWrapper} potentially indicating a nullable cached value).
* @param key the key whose associated value is to be returned
* @return the value to which this cache maps the specified key,
* contained within a {@link CompletableFuture} which may also hold
* a cached {@code null} value. A straight {@code null} being
* returned means that the cache contains no mapping for this key.
* @return the value to which this cache maps the specified key, contained
* within a {@link CompletableFuture} which may also be empty when a cache
* miss has been late-determined. A straight {@code null} being returned
* means that the cache immediately determined that it contains no mapping
* for this key. A {@link ValueWrapper} contained within the
* {@code CompletableFuture} can indicate a cached value that is potentially
* {@code null}; this is sensible in a late-determined scenario where a regular
* CompletableFuture-contained {@code null} indicates a cache miss. However,
* an early-determined cache will usually return the plain cached value here,
* and a late-determined cache may also return a plain value if it does not
* support the actual caching of {@code null} values. Spring's common cache
* processing can deal with all variants of these implementation strategies.
* @since 6.1
* @see #get(Object)
* @see #retrieve(Object, Supplier)
*/
@Nullable
default CompletableFuture<?> retrieve(Object key) {
Expand Down

0 comments on commit 1410c46

Please sign in to comment.