caffeine cache for loadbalancer Fix gh 638 (#643)

* Add Caffeine-based loadBalancerCacheManager.

* Add more tests.

* Fix test.

* Only enable loadBalancer caching if caffeine is on classpath. Warn if
caffeine is not on classpath.

* Add javadocs.

* Add docs. Change property name.

* Fix configuration.

* Fix after code review. Add another constructor to CaffeineBasedLoadBalancerCacheManager,
allowing to pass cache name, in order to support alternative ServiceInstanceListSupplierImplementations.
This commit is contained in:
Olga Maciaszek-Sharma
2019-11-22 15:53:57 +01:00
committed by GitHub
parent d4cc825aa0
commit bf6b31ee8a
8 changed files with 334 additions and 57 deletions

View File

@@ -801,6 +801,28 @@ in the following sections:
* <<webclinet-loadbalancer-client, Spring WebClient as a Load Balancer Client>>
* <<webflux-with-reactive-loadbalancer,Spring WebFlux WebClient with `ReactorLoadBalancerExchangeFilterFunction`>>
=== Spring Cloud LoadBalancer Caching
Apart from the basic `ServiceInstanceListSupplier` implementation that retrieves instances
via `DiscoveryClient` each time it has to choose an instance, we provide a https://github.com/ben-manes/caffeine[Caffeine-backed]
implementation.
To make use of it, you need to have `com.github.ben-manes.caffeine:caffeine` in the classpath.
The default setup includes `expireAfterWrite` set to 30 seconds and records set to soft references.
You can set your own `TTL` value (the time after write after which entries should be expired), expressed as `Duration`, by passing a `String` compliant with the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[`Duration` API]
as the value of `spring.cloud.loadbalancer.cache.ttl` property.
You can also override the default Caffeine Cache setup for the LoadBalancer by passing your own https://static.javadoc.io/com.github.ben-manes.caffeine/caffeine/2.2.2/com/github/benmanes/caffeine/cache/CaffeineSpec.html[Caffeine Specification]
in the `spring.cloud.loadbalancer.cache.caffeine.spec` property.
WARN: Passing your own Caffeine specification will override any other LoadBalancerCache settings, including `TTL`.
You can also altogether disable loadBalancer caching by setting the value of `spring.cloud.loadbalancer.cache.enabled`
to `false`.
WARNING: Although the basic, non-cached, implementation is useful for prototyping and testing, it's much less efficient
than the cached versions, so we recommend always using the cached version in production.
[[spring-cloud-loadbalancer-starter]]
=== Spring Cloud LoadBalancer starter
@@ -808,11 +830,8 @@ We also provide a starter that allows you to easily add Spring Cloud LoadBalance
In order to use it, just add `org.springframework.cloud:spring-cloud-starter-loadbalancer` to your Spring
Cloud dependencies in your build file.
WARNING: In order to make use of the more efficient cached version of `ServiceInstanceListSupplier`,
`spring-cloud-starter-loadbalancer` will *enable caching* by default.
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html[Spring Boot Caching]
mechanism will be used under the hood. If you don't want caching to be used, you can set
the value of `spring.cache.type` to `none`.
NOTE: Spring Cloud LoadBalancer starter includes
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html[Spring Boot Caching].
WARNING: If you have both Ribbon and Spring Cloud LoadBalancer int the classpath, in order to maintain
backward compatibility, Ribbon-based implementations will be used by default. In order

View File

@@ -61,7 +61,12 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -78,10 +83,5 @@
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -20,13 +20,13 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled;
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceSupplier;
import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier;
@@ -36,6 +36,7 @@ import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@@ -73,12 +74,14 @@ public class LoadBalancerClientConfiguration {
@ConditionalOnMissingBean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ReactiveDiscoveryClient discoveryClient, Environment env,
ObjectProvider<CacheManager> cacheManager) {
ApplicationContext context) {
DiscoveryClientServiceInstanceListSupplier delegate = new DiscoveryClientServiceInstanceListSupplier(
discoveryClient, env);
if (cacheManager.getIfAvailable() != null) {
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(delegate,
cacheManager.getIfAvailable());
cacheManagerProvider.getIfAvailable());
}
return delegate;
}
@@ -88,12 +91,14 @@ public class LoadBalancerClientConfiguration {
@ConditionalOnMissingBean
public ServiceInstanceSupplier discoveryClientServiceInstanceSupplier(
ReactiveDiscoveryClient discoveryClient, Environment env,
ObjectProvider<CacheManager> cacheManager) {
ApplicationContext context) {
DiscoveryClientServiceInstanceSupplier delegate = new DiscoveryClientServiceInstanceSupplier(
discoveryClient, env);
if (cacheManager.getIfAvailable() != null) {
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceSupplier(delegate,
cacheManager.getIfAvailable());
cacheManagerProvider.getIfAvailable());
}
return delegate;
}
@@ -110,12 +115,14 @@ public class LoadBalancerClientConfiguration {
@ConditionalOnMissingBean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
DiscoveryClient discoveryClient, Environment env,
ObjectProvider<CacheManager> cacheManager) {
ApplicationContext context) {
DiscoveryClientServiceInstanceListSupplier delegate = new DiscoveryClientServiceInstanceListSupplier(
discoveryClient, env);
if (cacheManager.getIfAvailable() != null) {
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(delegate,
cacheManager.getIfAvailable());
cacheManagerProvider.getIfAvailable());
}
return delegate;
}
@@ -125,12 +132,14 @@ public class LoadBalancerClientConfiguration {
@ConditionalOnMissingBean
public ServiceInstanceSupplier discoveryClientServiceInstanceSupplier(
DiscoveryClient discoveryClient, Environment env,
ObjectProvider<CacheManager> cacheManager) {
ApplicationContext context) {
DiscoveryClientServiceInstanceSupplier delegate = new DiscoveryClientServiceInstanceSupplier(
discoveryClient, env);
if (cacheManager.getIfAvailable() != null) {
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceSupplier(delegate,
cacheManager.getIfAvailable());
cacheManagerProvider.getIfAvailable());
}
return delegate;
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2019 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.loadbalancer.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier.SERVICE_INSTANCE_CACHE_NAME;
/**
* A Spring Cloud LoadBalancer specific implementation of {@link CaffeineCacheManager} that
* implements the {@link LoadBalancerCacheManager} marker interface.
*
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
public class CaffeineBasedLoadBalancerCacheManager extends CaffeineCacheManager
implements LoadBalancerCacheManager {
public CaffeineBasedLoadBalancerCacheManager(String cacheName,
LoadBalancerCacheProperties properties) {
super(cacheName);
if (!StringUtils.isEmpty(properties.getCaffeine().getSpec())) {
setCacheSpecification(properties.getCaffeine().getSpec());
}
else {
setCaffeine(Caffeine.newBuilder().expireAfterWrite(properties.getTtl())
.softValues());
}
}
public CaffeineBasedLoadBalancerCacheManager(LoadBalancerCacheProperties properties) {
this(SERVICE_INSTANCE_CACHE_NAME, properties);
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2012-2019 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.loadbalancer.cache;
import org.springframework.cache.CacheManager;
/**
* A marker interface for Spring Cloud LoadBalancer-specific {@link CacheManager} beans.
*
* @author Olga Maciaszek-Sharma
*/
public interface LoadBalancerCacheManager extends CacheManager {
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2012-2019 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.loadbalancer.cache;
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Spring Cloud LoadBalancer cache properties.
*
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
@ConfigurationProperties("spring.cloud.loadbalancer.cache")
public class LoadBalancerCacheProperties {
private Caffeine caffeine = new Caffeine();
/**
* Time To Live - time counted from writing of the record, after which cache entries
* are expired, expressed as a {@link Duration}. The property {@link String} has to be
* in keeping with the appropriate syntax as specified in
* {@link Duration#parse(CharSequence)}.
*/
private Duration ttl = Duration.ofSeconds(30);
public Caffeine getCaffeine() {
return caffeine;
}
public void setCaffeine(Caffeine caffeine) {
this.caffeine = caffeine;
}
public Duration getTtl() {
return ttl;
}
public void setTtl(String ttl) {
this.ttl = Duration.parse(ttl);
}
/**
* Caffeine-specific LoadBalancer cache properties.
* NOTE: Passing your own Caffeine specification will override any other LoadBalancerCache settings,
* including TTL.
*/
public static class Caffeine {
/**
* The spec to use to create caches. See CaffeineSpec for more details on the spec
* format.
*/
private String spec = "";
public String getSpec() {
return spec;
}
public void setSpec(String spec) {
this.spec = spec;
}
}
}

View File

@@ -16,29 +16,85 @@
package org.springframework.cloud.loadbalancer.config;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import javax.annotation.PostConstruct;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheAspectSupport;
import org.springframework.cloud.loadbalancer.cache.CaffeineBasedLoadBalancerCacheManager;
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* An AutoConfiguration that automatically enables caching when when Spring Boot and
* Spring Framework Cache support classes are present.
* An AutoConfiguration that automatically enables caching when when Spring Boot, and
* Spring Framework Cache support and Caffeine classes are present and warns if Caffeine
* is not present (we are only warning about Caffeine because the other dependencies are
* in spring-cloud-starter-loadbalancer).
*
* @author Olga Maciaszek-Sharma
* @since 2.2.0
* @see CacheManager
* @see CacheAutoConfiguration
* @see CacheAspectSupport
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ CacheManager.class, CacheAutoConfiguration.class })
@ConditionalOnMissingBean(CacheAspectSupport.class)
@EnableCaching
@AutoConfigureBefore(CacheAutoConfiguration.class)
@AutoConfigureAfter(CacheAutoConfiguration.class)
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.cache.enabled",
matchIfMissing = true)
@EnableConfigurationProperties(LoadBalancerCacheProperties.class)
public class LoadBalancerCacheAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("com.github.benmanes.caffeine.cache.Caffeine")
protected static class LoadBalancerCacheManagerWarnConfiguration {
@Bean
LoadBalancerCaffeineWarnLogger caffeineWarnLogger() {
return new LoadBalancerCaffeineWarnLogger();
}
}
static class LoadBalancerCaffeineWarnLogger {
private static final Log LOG = LogFactory
.getLog(LoadBalancerCaffeineWarnLogger.class);
@PostConstruct
void logWarning() {
if (LOG.isWarnEnabled()) {
LOG.warn(
"Spring Cloud LoadBalancer is currently working without cache. To enable cache, add "
+ "com.github.ben-manes.caffeine:caffeine dependency to classpath.");
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Caffeine.class)
protected static class LoadBalancerCacheManagerConfiguration {
@Bean(autowireCandidate = false)
@ConditionalOnMissingBean
LoadBalancerCacheManager loadBalancerCacheManager(
LoadBalancerCacheProperties cacheProperties) {
return new CaffeineBasedLoadBalancerCacheManager(cacheProperties);
}
}
}

View File

@@ -16,17 +16,16 @@
package org.springframework.cloud.loadbalancer.config;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.support.NoOpCacheManager;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.util.StringUtils;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
@@ -40,31 +39,63 @@ class LoadBalancerCacheAutoConfigurationTests {
@Test
void shouldAutoEnableCaching() {
AnnotationConfigApplicationContext context = setup("");
assertThat(context.getBeansOfType(CacheManager.class)).isNotEmpty();
assertThat(context.getBeansOfType(CacheManager.class).get("cacheManager"))
.isNotInstanceOf(NoOpCacheManager.class);
ApplicationContextRunner contextRunner = baseApplicationRunner();
contextRunner.run(context -> {
assertThat(context.getBeansOfType(CacheManager.class)).hasSize(1);
assertThat(((CacheManager) context.getBean("loadBalancerCacheManager"))
.getCacheNames()).hasSize(1);
assertThat(context.getBean("loadBalancerCacheManager"))
.isInstanceOf(CaffeineCacheManager.class);
assertThat(((CacheManager) context.getBean("loadBalancerCacheManager"))
.getCacheNames()).contains("CachingServiceInstanceListSupplierCache");
});
}
@Test
void shouldUseNoOpCacheIfCacheTypeNone() {
AnnotationConfigApplicationContext context = setup("spring.cache.type=none");
assertThat(context.getBeansOfType(CacheManager.class)).isNotEmpty();
assertThat(context.getBeansOfType(CacheManager.class).get("cacheManager"))
.isInstanceOf(NoOpCacheManager.class);
void loadBalancerCacheShouldNotOverrideCacheTypeSetting() {
ApplicationContextRunner contextRunner = baseApplicationRunner()
.withUserConfiguration(TestConfiguration.class)
.withPropertyValues("spring.cache.type=none");
contextRunner.run(context -> {
assertThat(context.getBeansOfType(CacheManager.class)).hasSize(2);
assertThat(context.getBean("loadBalancerCacheManager"))
.isInstanceOf(CaffeineCacheManager.class);
assertThat(context.getBeansOfType(CacheManager.class).get("cacheManager"))
.isInstanceOf(NoOpCacheManager.class);
});
}
private AnnotationConfigApplicationContext setup(String property) {
List<Class> config = new ArrayList<>();
config.add(LoadBalancerCacheAutoConfiguration.class);
config.add(CacheAutoConfiguration.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (StringUtils.hasText(property)) {
TestPropertyValues.of(property).applyTo(context);
}
context.register(config.toArray(new Class[0]));
context.refresh();
return context;
@Test
void loadBalancerCacheShouldNotOverrideExistingCaffeineCacheManager() {
ApplicationContextRunner contextRunner = baseApplicationRunner()
.withUserConfiguration(TestConfiguration.class);
contextRunner.run(context -> {
assertThat(context.getBeansOfType(CacheManager.class)).hasSize(2);
assertThat(context.getBean("cacheManager"))
.isInstanceOf(CaffeineCacheManager.class);
assertThat(((CacheManager) context.getBean("cacheManager")).getCacheNames())
.isEmpty();
assertThat(((CacheManager) context.getBean("loadBalancerCacheManager"))
.getCacheNames()).hasSize(1);
assertThat(((CacheManager) context.getBean("loadBalancerCacheManager"))
.getCacheNames()).contains("CachingServiceInstanceListSupplierCache");
});
}
private ApplicationContextRunner baseApplicationRunner() {
return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(
CacheAutoConfiguration.class, LoadBalancerCacheAutoConfiguration.class));
}
@Configuration(proxyBeanMethods = false)
@EnableCaching
static class TestConfiguration {
}
}