Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Weigher;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.ObjectProvider;
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.CaffeineCache;
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.LocalResponseCacheGatewayFilterFactory.CacheMetricsListener;
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties;
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheUtils;
import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheManagerFactory;
Expand All @@ -50,8 +51,6 @@
@ConditionalOnEnabledFilter(LocalResponseCacheGatewayFilterFactory.class)
public class LocalResponseCacheAutoConfiguration {

private static final Log LOGGER = LogFactory.getLog(LocalResponseCacheAutoConfiguration.class);

private static final String RESPONSE_CACHE_NAME = "response-cache";

/* for testing */ static final String RESPONSE_CACHE_MANAGER_NAME = "gatewayCacheManager";
Expand All @@ -60,10 +59,15 @@ public class LocalResponseCacheAutoConfiguration {
@Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class)
public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter(
ResponseCacheManagerFactory responseCacheManagerFactory,
@Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager,
LocalResponseCacheProperties properties) {
return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, responseCache(cacheManager),
properties.getTimeToLive(), properties.getRequest());
@Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager, LocalResponseCacheProperties properties,
ObjectProvider<CacheMetricsListener> metricsListenerProvider) {
Cache cache = responseCache(cacheManager);
CacheMetricsListener listener = metricsListenerProvider.getIfAvailable(() -> CacheMetricsListener.NOOP);
if (cache instanceof CaffeineCache caffeineCache) {
listener.onCacheCreated(caffeineCache.getNativeCache(), RESPONSE_CACHE_NAME);
}
return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, cache, properties.getTimeToLive(),
properties.getRequest());
}

@Bean(name = RESPONSE_CACHE_MANAGER_NAME)
Expand All @@ -74,9 +78,11 @@ public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProper

@Bean
public LocalResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory(
ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties) {
ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties,
ObjectProvider<CacheMetricsListener> metricsListenerProvider) {
CacheMetricsListener listener = metricsListenerProvider.getIfAvailable(() -> CacheMetricsListener.NOOP);
return new LocalResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(),
properties.getSize(), properties.getRequest());
properties.getSize(), properties.getRequest(), new CaffeineCacheManager(), listener);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2013-present 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.config;

import java.util.Collections;

import com.github.benmanes.caffeine.cache.Caffeine;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory.CacheMetricsListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.DispatcherHandler;

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".enabled", matchIfMissing = true)
@AutoConfigureAfter({ LocalResponseCacheAutoConfiguration.class, MetricsAutoConfiguration.class,
CompositeMeterRegistryAutoConfiguration.class })
@ConditionalOnClass({ DispatcherHandler.class, Caffeine.class, CaffeineCacheManager.class, MeterRegistry.class,
MetricsAutoConfiguration.class })
public class LocalResponseCacheMetricsAutoConfiguration {

@Bean
@ConditionalOnBean(MeterRegistry.class)
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".metrics.enabled", matchIfMissing = true)
public CacheMetricsListener localResponseCacheMetricsListener(MeterRegistry meterRegistry) {
return (cache, cacheName) -> CaffeineCacheMetrics.monitor(meterRegistry, cache, cacheName,
Collections.emptyList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,31 @@ public class LocalResponseCacheGatewayFilterFactory

private final CaffeineCacheManager caffeineCacheManager;

private final CacheMetricsListener cacheMetricsListener;

public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory,
Duration defaultTimeToLive, DataSize defaultSize, RequestOptions requestOptions) {
this(cacheManagerFactory, defaultTimeToLive, defaultSize, requestOptions, new CaffeineCacheManager());
this(cacheManagerFactory, defaultTimeToLive, defaultSize, requestOptions, new CaffeineCacheManager(),
CacheMetricsListener.NOOP);
}

public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory,
Duration defaultTimeToLive, DataSize defaultSize, RequestOptions requestOptions,
CaffeineCacheManager caffeineCacheManager) {
this(cacheManagerFactory, defaultTimeToLive, defaultSize, requestOptions, caffeineCacheManager,
CacheMetricsListener.NOOP);
}

public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory,
Duration defaultTimeToLive, DataSize defaultSize, RequestOptions requestOptions,
CaffeineCacheManager caffeineCacheManager, CacheMetricsListener cacheMetricsListener) {
super(RouteCacheConfiguration.class);
this.cacheManagerFactory = cacheManagerFactory;
this.defaultTimeToLive = defaultTimeToLive;
this.defaultSize = defaultSize;
this.requestOptions = requestOptions;
this.caffeineCacheManager = caffeineCacheManager;
this.cacheMetricsListener = cacheMetricsListener;
}

@Override
Expand All @@ -86,7 +97,9 @@ public GatewayFilter apply(RouteCacheConfiguration config) {

Caffeine caffeine = LocalResponseCacheUtils.createCaffeine(cacheProperties);
String cacheName = config.getRouteId() + "-cache";
caffeineCacheManager.registerCustomCache(cacheName, caffeine.build());
com.github.benmanes.caffeine.cache.Cache nativeCache = caffeine.build();
caffeineCacheManager.registerCustomCache(cacheName, nativeCache);
cacheMetricsListener.onCacheCreated(nativeCache, cacheName);
Cache routeCache = caffeineCacheManager.getCache(cacheName);
Objects.requireNonNull(routeCache, "Cache " + cacheName + " not found");
return new ResponseCacheGatewayFilter(
Expand All @@ -109,6 +122,16 @@ public List<String> shortcutFieldOrder() {
return List.of("timeToLive", "size");
}

@FunctionalInterface
public interface CacheMetricsListener {

void onCacheCreated(com.github.benmanes.caffeine.cache.Cache<?, ?> cache, String cacheName);

CacheMetricsListener NOOP = (cache, cacheName) -> {
};

}

@Validated
public static class RouteCacheConfiguration implements HasRouteId {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public static CaffeineCacheManager createGatewayCacheManager(LocalResponseCacheP

@SuppressWarnings({ "unchecked", "rawtypes" })
public static Caffeine createCaffeine(LocalResponseCacheProperties cacheProperties) {
Caffeine caffeine = Caffeine.newBuilder();
// Record stats unconditionally; LongAdder overhead is negligible.
Caffeine caffeine = Caffeine.newBuilder().recordStats();
LOGGER.info("Initializing Caffeine");
Duration ttlSeconds = cacheProperties.getTimeToLive();
caffeine.expireAfterWrite(ttlSeconds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfigurat
org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration
org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration
org.springframework.cloud.gateway.config.LocalResponseCacheAutoConfiguration
org.springframework.cloud.gateway.config.LocalResponseCacheMetricsAutoConfiguration
org.springframework.cloud.gateway.config.GatewayTracingAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright 2013-present 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.config;

import java.time.Duration;

import com.github.benmanes.caffeine.cache.Caffeine;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory.CacheMetricsListener;
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties;
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheUtils;
import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheManagerFactory;
import org.springframework.cloud.gateway.filter.factory.cache.keygenerator.CacheKeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
* Tests for {@link LocalResponseCacheMetricsAutoConfiguration}.
*
* @author LivingLikeKrillin
*/
public class LocalResponseCacheMetricsAutoConfigurationTests {

@Test
void metricsListenerCreatedWhenMeterRegistryPresent() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(LocalResponseCacheAutoConfiguration.class,
LocalResponseCacheMetricsAutoConfiguration.class))
.withUserConfiguration(MeterRegistryConfig.class)
.withPropertyValues(GatewayProperties.PREFIX + ".filter.local-response-cache.enabled=true")
.run(context -> {
assertThat(context).hasSingleBean(CacheMetricsListener.class);
});
}

@Test
void metricsListenerNotCreatedWhenMeterRegistryAbsent() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(LocalResponseCacheAutoConfiguration.class,
LocalResponseCacheMetricsAutoConfiguration.class))
.withPropertyValues(GatewayProperties.PREFIX + ".filter.local-response-cache.enabled=true")
.run(context -> {
assertThat(context).doesNotHaveBean(CacheMetricsListener.class);
});
}

@Test
void metricsListenerNotCreatedWhenGatewayDisabled() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(LocalResponseCacheAutoConfiguration.class,
LocalResponseCacheMetricsAutoConfiguration.class))
.withUserConfiguration(MeterRegistryConfig.class)
.withPropertyValues(GatewayProperties.PREFIX + ".filter.local-response-cache.enabled=true",
GatewayProperties.PREFIX + ".enabled=false")
.run(context -> {
assertThat(context).doesNotHaveBean(CacheMetricsListener.class);
});
}

@Test
void metricsListenerNotCreatedWhenMetricsDisabled() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(LocalResponseCacheAutoConfiguration.class,
LocalResponseCacheMetricsAutoConfiguration.class))
.withUserConfiguration(MeterRegistryConfig.class)
.withPropertyValues(GatewayProperties.PREFIX + ".filter.local-response-cache.enabled=true",
GatewayProperties.PREFIX + ".metrics.enabled=false")
.run(context -> {
assertThat(context).doesNotHaveBean(CacheMetricsListener.class);
});
}

@Test
void caffeineRecordStatsEnabled() {
LocalResponseCacheProperties properties = new LocalResponseCacheProperties();
properties.setTimeToLive(Duration.ofMinutes(5));
Caffeine<Object, Object> caffeine = LocalResponseCacheUtils.createCaffeine(properties);
com.github.benmanes.caffeine.cache.Cache<Object, Object> cache = caffeine.build();

cache.put("key", "value");
cache.getIfPresent("key");
cache.getIfPresent("missing");

assertThat(cache.stats().hitCount()).isEqualTo(1);
assertThat(cache.stats().missCount()).isEqualTo(1);
}

@Test
void cacheMetricsListenerBindsToMeterRegistry() {
SimpleMeterRegistry registry = new SimpleMeterRegistry();
LocalResponseCacheProperties properties = new LocalResponseCacheProperties();
properties.setTimeToLive(Duration.ofMinutes(5));
Caffeine<Object, Object> caffeine = LocalResponseCacheUtils.createCaffeine(properties);
com.github.benmanes.caffeine.cache.Cache<Object, Object> cache = caffeine.build();

new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(LocalResponseCacheMetricsAutoConfiguration.class))
.withBean(MeterRegistry.class, () -> registry)
.run(context -> {
CacheMetricsListener listener = context.getBean(CacheMetricsListener.class);
listener.onCacheCreated(cache, "test-cache");

cache.put("key", "value");
cache.getIfPresent("key");

assertThat(registry.find("cache.gets").tag("result", "hit").functionCounter()).isNotNull();
assertThat(registry.find("cache.size").tag("cache", "test-cache").gauge()).isNotNull();
});
}

@Test
void globalCacheMetricsRegisteredViaCaffeineCache() {
SimpleMeterRegistry registry = new SimpleMeterRegistry();
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(LocalResponseCacheAutoConfiguration.class,
LocalResponseCacheMetricsAutoConfiguration.class))
.withBean(MeterRegistry.class, () -> registry)
.withPropertyValues(GatewayProperties.PREFIX + ".filter.local-response-cache.enabled=true",
GatewayProperties.PREFIX + ".enabled=true",
GatewayProperties.PREFIX + ".global-filter.local-response-cache.enabled=true")
.run(context -> {
assertThat(context).hasSingleBean(CacheMetricsListener.class);
assertThat(registry.find("cache.size").tag("cache", "response-cache").gauge()).isNotNull();
});
}

@Test
void perRouteCacheMetricsRegisteredViaFilterFactory() {
SimpleMeterRegistry registry = new SimpleMeterRegistry();
CacheMetricsListener listener = (cache,
cacheName) -> io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics.monitor(registry, cache,
cacheName, java.util.Collections.emptyList());

Duration ttl = Duration.ofMinutes(5);
ResponseCacheManagerFactory cacheManagerFactory = new ResponseCacheManagerFactory(new CacheKeyGenerator());
LocalResponseCacheGatewayFilterFactory factory = new LocalResponseCacheGatewayFilterFactory(cacheManagerFactory,
ttl, null, new LocalResponseCacheProperties.RequestOptions(), new CaffeineCacheManager(), listener);

LocalResponseCacheGatewayFilterFactory.RouteCacheConfiguration routeConfig = new LocalResponseCacheGatewayFilterFactory.RouteCacheConfiguration();
routeConfig.setRouteId("my-route");
routeConfig.setTimeToLive(ttl);
factory.apply(routeConfig);

assertThat(registry.find("cache.size").tag("cache", "my-route-cache").gauge()).isNotNull();
}

@Configuration(proxyBeanMethods = false)
static class MeterRegistryConfig {

@Bean
MeterRegistry meterRegistry() {
return new SimpleMeterRegistry();
}

}

}