Merge branch 'mmedio/fix-global-cache'

This commit is contained in:
spencergibb
2023-02-14 14:20:41 -05:00
8 changed files with 153 additions and 31 deletions

View File

@@ -2166,10 +2166,11 @@ NOTE: To enable the prometheus endpoint, add `micrometer-registry-prometheus` as
[[local-cache-response-global-filter]]
=== The Local Response Cache Filter
The `LocalResponseCache` runs if its associated property is enabled (`spring.cloud.gateway.filter.local-response-cache.enabled`) and activates a local cache for all responses that meet the following criteria:
- The request is a bodiless GET.
- The response has one of the following status codes: HTTP 200 (OK), HTTP 206 (Partial Content), or HTTP 301 (Moved Permanently).
- The HTTP `Cache-Control` header allows caching (that means it does not have any of the following values: `no-store` present in the request and `no-store` or `private` present in the response).
The `LocalResponseCache` runs if its associated property is enabled (`spring.cloud.gateway.filter.local-response-cache.enabled`) and activates a local cache using Caffeine for all responses that meet the following criteria:
* The request is a bodiless GET.
* The response has one of the following status codes: HTTP 200 (OK), HTTP 206 (Partial Content), or HTTP 301 (Moved Permanently).
* The HTTP `Cache-Control` header allows caching (that means it does not have any of the following values: `no-store` present in the request and `no-store` or `private` present in the response).
It accepts two configuration parameters:
@@ -2178,10 +2179,12 @@ It accepts two configuration parameters:
If none of these parameters are configured but the global filter is enabled, by default, it configures 5 minutes of time to live for the cached response.
This filter also implements the automatic calculation of the `max-age value in the HTTP `Cache-Control` header.
This filter also implements the automatic calculation of the `max-age` value in the HTTP `Cache-Control` header.
If `max-age` is present on the original response, the value is rewritten with the number of seconds set in the `timeToLive` configuration parameter.
In subsequent calls, this value is recalculated with the number of seconds left until the response expires.
NOTE: To enable this feature, add `com.github.ben-manes.caffeine:caffeine` and `spring-boot-starter-cache` as project dependencies.
WARNING: If your project creates custom `CacheManager` beans, it will either need to be marked with `@Primary` or injected using `@Qualifier`.
=== Forward Routing Filter

View File

@@ -24,29 +24,31 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledFilter;
import org.springframework.cloud.gateway.filter.factory.cache.GlobalLocalResponseCacheGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties;
import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheManagerFactory;
import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheSizeWeigher;
import org.springframework.cloud.gateway.filter.factory.cache.keygenerator.CacheKeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
/**
* @author Ignacio Lozano
* @author Marta Medio
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ LocalResponseCacheProperties.class })
@ConditionalOnClass({ Weigher.class, Caffeine.class, CaffeineCacheManager.class })
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@ConditionalOnEnabledFilter(LocalResponseCacheGatewayFilterFactory.class)
@Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class)
public class LocalResponseCacheAutoConfiguration {
private static final Log LOGGER = LogFactory.getLog(LocalResponseCacheAutoConfiguration.class);
@@ -55,6 +57,15 @@ public class LocalResponseCacheAutoConfiguration {
/* for testing */ static final String RESPONSE_CACHE_MANAGER_NAME = "gatewayCacheManager";
@Bean
public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter(
ResponseCacheManagerFactory responseCacheManagerFactory,
@Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager,
LocalResponseCacheProperties properties) {
return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, responseCache(cacheManager),
properties.getTimeToLive());
}
@Bean
public LocalResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory(
ResponseCacheManagerFactory responseCacheManagerFactory,
@@ -75,13 +86,12 @@ public class LocalResponseCacheAutoConfiguration {
}
@Bean(name = RESPONSE_CACHE_MANAGER_NAME)
public static CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) {
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(caffeine(cacheProperties));
return caffeineCacheManager;
public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) {
return createGatewayCacheManager(cacheProperties);
}
private static Caffeine caffeine(LocalResponseCacheProperties cacheProperties) {
@SuppressWarnings({ "unchecked", "rawtypes" })
public static CaffeineCacheManager createGatewayCacheManager(LocalResponseCacheProperties cacheProperties) {
Caffeine caffeine = Caffeine.newBuilder();
LOGGER.info("Initializing Caffeine");
Duration ttlSeconds = cacheProperties.getTimeToLive();
@@ -90,7 +100,9 @@ public class LocalResponseCacheAutoConfiguration {
if (cacheProperties.getSize() != null) {
caffeine.maximumWeight(cacheProperties.getSize().toBytes()).weigher(responseCacheSizeWeigher());
}
return caffeine;
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(caffeine);
return caffeineCacheManager;
}
private static ResponseCacheSizeWeigher responseCacheSizeWeigher() {
@@ -101,4 +113,22 @@ public class LocalResponseCacheAutoConfiguration {
return cacheManager.getCache(RESPONSE_CACHE_NAME);
}
public static class OnGlobalLocalResponseCacheCondition extends AllNestedConditions {
OnGlobalLocalResponseCacheCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(value = "spring.cloud.gateway.enabled", havingValue = "true", matchIfMissing = true)
static class OnGatewayPropertyEnabled {
}
@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true")
static class OnLocalResponseCachePropertyEnabled {
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2013-2020 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.cloud.gateway.filter.factory.cache;
import java.time.Duration;
import reactor.core.publisher.Mono;
import org.springframework.cache.Cache;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED;
/**
* Caches responses for routes that don't have the {@link LocalResponseCacheGatewayFilterFactory} configured.
* @author Ignacio Lozano
* @author Marta Medio
*/
public class GlobalLocalResponseCacheGatewayFilter implements GlobalFilter, Ordered {
private final ResponseCacheGatewayFilter responseCacheGatewayFilter;
public GlobalLocalResponseCacheGatewayFilter(ResponseCacheManagerFactory cacheManagerFactory, Cache globalCache,
Duration configuredTimeToLive) {
responseCacheGatewayFilter = new ResponseCacheGatewayFilter(
cacheManagerFactory.create(globalCache, configuredTimeToLive));
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (exchange.getAttributes().get(LOCAL_RESPONSE_CACHE_FILTER_APPLIED) == null) {
return responseCacheGatewayFilter.filter(exchange, chain);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 2;
}
}

View File

@@ -20,6 +20,7 @@ import java.time.Duration;
import java.util.List;
import java.util.Objects;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.Cache;
import org.springframework.cloud.gateway.config.LocalResponseCacheAutoConfiguration;
import org.springframework.cloud.gateway.filter.GatewayFilter;
@@ -39,9 +40,16 @@ import org.springframework.validation.annotation.Validated;
* @author Marta Medio
* @author Ignacio Lozano
*/
@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true")
public class LocalResponseCacheGatewayFilterFactory
extends AbstractGatewayFilterFactory<LocalResponseCacheGatewayFilterFactory.RouteCacheConfiguration> {
/**
* Exchange attribute name to track if the request has been already process by cache
* at route filter level.
*/
public static final String LOCAL_RESPONSE_CACHE_FILTER_APPLIED = "LocalResponseCacheGatewayFilter-Applied";
private final Cache globalCache;
ResponseCacheManagerFactory cacheManagerFactory;
@@ -64,7 +72,7 @@ public class LocalResponseCacheGatewayFilterFactory
return new ResponseCacheGatewayFilter(cacheManagerFactory.create(globalCache, configuredTimeToLive));
}
else {
Cache routeCache = LocalResponseCacheAutoConfiguration.gatewayCacheManager(cacheProperties)
Cache routeCache = LocalResponseCacheAutoConfiguration.createGatewayCacheManager(cacheProperties)
.getCache(config.getRouteId() + "-cache");
return new ResponseCacheGatewayFilter(
cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive()));

View File

@@ -31,6 +31,8 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED;
/**
* {@literal LocalResponseCache} Gateway Filter that stores HTTP Responses in a cache, so
* latency and upstream overhead is reduced.
@@ -49,6 +51,7 @@ public class ResponseCacheGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (responseCacheManager.isRequestCacheable(exchange.getRequest())) {
exchange.getAttributes().put(LOCAL_RESPONSE_CACHE_FILTER_APPLIED, true);
return filterWithCache(exchange, chain);
}
else {
@@ -58,7 +61,7 @@ public class ResponseCacheGatewayFilter implements GatewayFilter, Ordered {
@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 3;
}
private Mono<Void> filterWithCache(ServerWebExchange exchange, GatewayFilterChain chain) {

View File

@@ -39,8 +39,19 @@
{
"name": "spring.cloud.gateway.filter.local-response-cache.enabled",
"type": "java.lang.Boolean",
"description": "Enables the local-response-cache filter.",
"defaultValue": "true"
"description": "Enables the local-response-cache filter for all routes, it allows to add a specific configuration at route level using LocalResponseCache filter.",
"defaultValue": "false"
},
{
"name": "spring.cloud.gateway.filter.local-response-cache.size",
"type": "org.springframework.util.unit.DataSize",
"description": "Maximum size of the cache to evict entries for this route (in KB, MB and GB).",
"defaultValue": "5m"
},
{
"name": "spring.cloud.gateway.filter.local-response-cache.timeToLive",
"type": "java.time.Duration",
"description": "Time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours)."
},
{
"name": "spring.cloud.gateway.filter.dedupe-response-header.enabled",

View File

@@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cloud.gateway.filter.factory.cache.GlobalLocalResponseCacheGatewayFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@@ -32,16 +33,21 @@ public class LocalResponseCacheAutoConfigurationTests {
void onlyOneCacheManagerBeanCreated() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(LocalResponseCacheAutoConfiguration.class))
.run(context -> context.containsBean(LocalResponseCacheAutoConfiguration.RESPONSE_CACHE_MANAGER_NAME));
.withPropertyValues("spring.cloud.gateway.filter.local-response-cache.enabled=true").run(context -> {
context.containsBean(LocalResponseCacheAutoConfiguration.RESPONSE_CACHE_MANAGER_NAME);
context.assertThat().hasSingleBean(GlobalLocalResponseCacheGatewayFilter.class);
});
}
@Test
void twoCacheManagerBeans() {
new ApplicationContextRunner().withConfiguration(
AutoConfigurations.of(CustomCacheManagerConfig.class, LocalResponseCacheAutoConfiguration.class))
.run(context -> {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CustomCacheManagerConfig.class,
LocalResponseCacheAutoConfiguration.class))
.withPropertyValues("spring.cloud.gateway.filter.local-response-cache.enabled=true").run(context -> {
context.containsBean(LocalResponseCacheAutoConfiguration.RESPONSE_CACHE_MANAGER_NAME);
context.containsBean("myCacheManager");
context.assertThat().hasSingleBean(GlobalLocalResponseCacheGatewayFilter.class);
});
}

View File

@@ -43,12 +43,13 @@ import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* @author Ignacio Lozano
* @author Marta Medio
*/
@SpringBootTest(webEnvironment = RANDOM_PORT)
@SpringBootTest(properties = { "spring.cloud.gateway.filter.local-response-cache.enabled=true" },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext
@ActiveProfiles(profiles = "local-cache-filter")
public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTests {
@@ -56,14 +57,14 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe
private static final String CUSTOM_HEADER = "X-Custom-Date";
@Test
void shouldNotCacheResponseWhenRouteDoesNotHaveFilter() {
String uri = "/" + UUID.randomUUID() + "/no-cache/headers";
void shouldGlobalCacheResponseWhenRouteDoesNotHaveFilter() {
String uri = "/" + UUID.randomUUID() + "/global-cache/headers";
testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
.expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2").exchange()
.expectBody().jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2");
.expectBody().jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("1");
}
@Test
@@ -251,13 +252,13 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("no_local_response_cache_java_test",
r -> r.path("/{namespace}/no-cache/**").and().host("{sub}.localresponsecache.org")
.route("global_local_response_cache_java_test",
r -> r.path("/{namespace}/global-cache/**").and().host("{sub}.localresponsecache.org")
.filters(f -> f.stripPrefix(2).prefixPath("/httpbin")).uri(uri))
.route("local_response_cache_java_test",
r -> r.path("/{namespace}/cache/**").and().host("{sub}.localresponsecache.org")
.filters(
f -> f.stripPrefix(2).prefixPath("/httpbin").localResponseCache(null, null))
.filters(f -> f.stripPrefix(2).prefixPath("/httpbin")
.localResponseCache(Duration.ofMinutes(2), null))
.uri(uri))
.route("100_millisec_ephemeral_prefix_local_response_cache_java_test",
r -> r.path("/{namespace}/ephemeral-cache/**").and().host("{sub}.localresponsecache.org")