Lb configuration builders (#751)

* Adds ServiceInstanceListSuppliers.java with Builder

* Implement TODOs.

* Add javadocs.

* Remove unused type.

* Add test.

* Safer caching config: resolve LoadBalancerCacheManager lazily. Return delegate if LoadBalancerCacheManager not available.

* Switch to using builder in LoadBalancerClientConfiguration.

* Autoformatting with spring-java-format.

* Update docs.

Co-authored-by: Spencer Gibb <spencer@gibb.us>
This commit is contained in:
Olga Maciaszek-Sharma
2020-05-05 17:55:03 +02:00
committed by GitHub
parent 5f413a7297
commit 8dff101d6f
8 changed files with 443 additions and 84 deletions

View File

@@ -917,22 +917,14 @@ public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ReactiveDiscoveryClient discoveryClient, Environment environment,
LoadBalancerZoneConfig zoneConfig,
ApplicationContext context) {
DiscoveryClientServiceInstanceListSupplier firstDelegate = new DiscoveryClientServiceInstanceListSupplier(
discoveryClient, environment);
ZonePreferenceServiceInstanceListSupplier delegate = new ZonePreferenceServiceInstanceListSupplier(firstDelegate,
zoneConfig);
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(delegate,
cacheManagerProvider.getIfAvailable());
}
return delegate;
}
ConfigurableApplicationContext context) {
return ServiceInstanceListSuppliers.builder()
.withDiscoveryClient()
.withZonePreference()
.withCaching()
.build(context);
}
}
----
=== Instance Health-Check for LoadBalancer
@@ -966,25 +958,18 @@ public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ReactiveDiscoveryClient discoveryClient, Environment environment,
LoadBalancerProperties loadBalancerProperties,
ApplicationContext context,
InstanceHealthChecker healthChecker) {
DiscoveryClientServiceInstanceListSupplier firstDelegate = new DiscoveryClientServiceInstanceListSupplier(
discoveryClient, environment);
HealthCheckServiceInstanceListSupplier delegate = new HealthCheckServiceInstanceListSupplier(firstDelegate,
loadBalancerProperties, healthChecker);
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(delegate,
cacheManagerProvider.getIfAvailable());
}
return delegate;
}
ConfigurableApplicationContext context) {
return ServiceInstanceListSuppliers.builder()
.withDiscoveryClient()
.withHealthChecks()
.withCaching()
.build(context);
}
}
----
TIP:: In order to make working on your own LoadBalancer configuration easier, we have added some utility methods in `ServiceInstanceListSuppliers` class.
[[spring-cloud-loadbalancer-starter]]
=== Spring Cloud LoadBalancer Starter

View File

@@ -26,16 +26,16 @@ 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;
import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSuppliers;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@@ -71,17 +71,9 @@ public class LoadBalancerClientConfiguration {
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ReactiveDiscoveryClient discoveryClient, Environment env,
ApplicationContext context) {
DiscoveryClientServiceInstanceListSupplier delegate = new DiscoveryClientServiceInstanceListSupplier(
discoveryClient, env);
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(delegate,
cacheManagerProvider.getIfAvailable());
}
return delegate;
ConfigurableApplicationContext context) {
return ServiceInstanceListSuppliers.builder().withDiscoveryClient()
.withCaching().build(context);
}
@Bean
@@ -112,17 +104,9 @@ public class LoadBalancerClientConfiguration {
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
DiscoveryClient discoveryClient, Environment env,
ApplicationContext context) {
DiscoveryClientServiceInstanceListSupplier delegate = new DiscoveryClientServiceInstanceListSupplier(
discoveryClient, env);
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(delegate,
cacheManagerProvider.getIfAvailable());
}
return delegate;
ConfigurableApplicationContext context) {
return ServiceInstanceListSuppliers.builder().withBlockingDiscoveryClient()
.withCaching().build(context);
}
@Bean

View File

@@ -37,7 +37,8 @@ import org.springframework.cloud.client.ServiceInstance;
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
public class CachingServiceInstanceListSupplier implements ServiceInstanceListSupplier {
public class CachingServiceInstanceListSupplier
extends DelegatingServiceInstanceListSupplier {
private static final Log log = LogFactory
.getLog(CachingServiceInstanceListSupplier.class);
@@ -48,14 +49,12 @@ public class CachingServiceInstanceListSupplier implements ServiceInstanceListSu
public static final String SERVICE_INSTANCE_CACHE_NAME = CachingServiceInstanceListSupplier.class
.getSimpleName() + "Cache";
private final ServiceInstanceListSupplier delegate;
private final Flux<List<ServiceInstance>> serviceInstances;
@SuppressWarnings("unchecked")
public CachingServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
CacheManager cacheManager) {
this.delegate = delegate;
super(delegate);
this.serviceInstances = CacheFlux.lookup(key -> {
// TODO: configurable cache name
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
@@ -70,7 +69,7 @@ public class CachingServiceInstanceListSupplier implements ServiceInstanceListSu
return Mono.empty();
}
return Flux.just(list).materialize().collectList();
}, delegate.getServiceId()).onCacheMissResume(this.delegate)
}, delegate.getServiceId()).onCacheMissResume(delegate)
.andWriteWith((key, signals) -> Flux.fromIterable(signals).dematerialize()
.doOnNext(instances -> {
Cache cache = cacheManager
@@ -87,11 +86,6 @@ public class CachingServiceInstanceListSupplier implements ServiceInstanceListSu
}).then());
}
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get() {
return serviceInstances;

View File

@@ -0,0 +1,46 @@
/*
* 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.core;
import org.springframework.util.Assert;
/**
* Represents a {@link ServiceInstanceListSupplier} that uses a delegate
* {@link ServiceInstanceListSupplier} instance underneath.
*
* @author Spencer Gibb
*/
public abstract class DelegatingServiceInstanceListSupplier
implements ServiceInstanceListSupplier {
private final ServiceInstanceListSupplier delegate;
public DelegatingServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
Assert.notNull(delegate, "delegate may not be null");
this.delegate = delegate;
}
public ServiceInstanceListSupplier getDelegate() {
return this.delegate;
}
@Override
public String getServiceId() {
return this.delegate.getServiceId();
}
}

View File

@@ -45,13 +45,12 @@ import org.springframework.web.util.UriComponentsBuilder;
* @since 2.2.0
*/
public class HealthCheckServiceInstanceListSupplier
implements ServiceInstanceListSupplier, InitializingBean, DisposableBean {
extends DelegatingServiceInstanceListSupplier
implements InitializingBean, DisposableBean {
private static final Log LOG = LogFactory
.getLog(HealthCheckServiceInstanceListSupplier.class);
private final ServiceInstanceListSupplier delegate;
private final LoadBalancerProperties.HealthCheck healthCheck;
private final WebClient webClient;
@@ -64,7 +63,7 @@ public class HealthCheckServiceInstanceListSupplier
public HealthCheckServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
LoadBalancerProperties.HealthCheck healthCheck, WebClient webClient) {
this.delegate = delegate;
super(delegate);
this.healthCheck = healthCheck;
defaultHealthCheckPath = healthCheck.getPath().getOrDefault("default",
"/actuator/health");
@@ -121,11 +120,6 @@ public class HealthCheckServiceInstanceListSupplier
}).repeatWhen(restart -> restart.delayElements(healthCheck.getInterval()));
}
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get() {
return aliveInstancesReplay;

View File

@@ -0,0 +1,246 @@
/*
* 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.loadbalancer.core;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
/**
* A utility class providing a {@link Builder} for creating a
* {@link ServiceInstanceListSupplier} hierarchy to be used in {@link ReactorLoadBalancer}
* configuration.
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
*/
public abstract class ServiceInstanceListSuppliers {
private ServiceInstanceListSuppliers() {
}
public static Builder builder() {
return new Builder();
}
/**
* Allows creating a {@link ServiceInstanceListSupplier} instance based on provided
* {@link ConfigurableApplicationContext}.
*/
public interface Creator extends
Function<ConfigurableApplicationContext, ServiceInstanceListSupplier> {
}
/**
* Allows creating a {@link ServiceInstanceListSupplier} instance based on provided
* {@link ConfigurableApplicationContext} and another
* {@link ServiceInstanceListSupplier} instance that will be used as a delegate.
*/
public interface DelegateCreator extends
BiFunction<ConfigurableApplicationContext, ServiceInstanceListSupplier, ServiceInstanceListSupplier> {
}
/**
* A builder for creating a {@link ServiceInstanceListSupplier} hierarchy to be used
* in {@link ReactorLoadBalancer} configuration.
*/
public static class Builder {
private static final Log LOG = LogFactory.getLog(Builder.class);
private Creator baseCreator;
private DelegateCreator cachingCreator;
private final List<DelegateCreator> creators = new ArrayList<>();
public Builder() {
}
/**
* Sets a blocking {@link DiscoveryClient}-based
* {@link DiscoveryClientServiceInstanceListSupplier} as a base
* {@link ServiceInstanceListSupplier} in the hierarchy.
* @return the {@link Builder} object
*/
public Builder withBlockingDiscoveryClient() {
if (baseCreator != null && LOG.isWarnEnabled()) {
LOG.warn(
"Overriding a previously set baseCreator with a blocking DiscoveryClient baseCreator.");
}
this.baseCreator = context -> {
DiscoveryClient discoveryClient = context.getBean(DiscoveryClient.class);
return new DiscoveryClientServiceInstanceListSupplier(discoveryClient,
context.getEnvironment());
};
return this;
}
/**
* Sets a {@link ReactiveDiscoveryClient}-based
* {@link DiscoveryClientServiceInstanceListSupplier} as a base
* {@link ServiceInstanceListSupplier} in the hierarchy.
* @return the {@link Builder} object
*/
public Builder withDiscoveryClient() {
if (baseCreator != null && LOG.isWarnEnabled()) {
LOG.warn(
"Overriding a previously set baseCreator with a ReactiveDiscoveryClient baseCreator.");
}
this.baseCreator = context -> {
ReactiveDiscoveryClient discoveryClient = context
.getBean(ReactiveDiscoveryClient.class);
return new DiscoveryClientServiceInstanceListSupplier(discoveryClient,
context.getEnvironment());
};
return this;
}
/**
* Sets a user-provided {@link ServiceInstanceListSupplier} as a base
* {@link ServiceInstanceListSupplier} in the hierarchy.
* @param supplier a user-provided {@link ServiceInstanceListSupplier} instance
* @return the {@link Builder} object
*/
public Builder withBase(ServiceInstanceListSupplier supplier) {
this.baseCreator = context -> supplier;
return this;
}
/**
* Adds a {@link HealthCheckServiceInstanceListSupplier} to the
* {@link ServiceInstanceListSupplier} hierarchy.
* @return the {@link Builder} object
*/
public Builder withHealthChecks() {
DelegateCreator creator = (context, delegate) -> {
LoadBalancerProperties properties = context
.getBean(LoadBalancerProperties.class);
WebClient.Builder webClient = context.getBean(WebClient.Builder.class);
return new HealthCheckServiceInstanceListSupplier(delegate,
properties.getHealthCheck(), webClient.build());
};
this.creators.add(creator);
return this;
}
/**
* Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided
* {@link WebClient} instance to the {@link ServiceInstanceListSupplier}
* hierarchy.
* @param webClient a user-provided {@link WebClient} instance
* @return the {@link Builder} object
*/
public Builder withHealthChecks(WebClient webClient) {
DelegateCreator creator = (context, delegate) -> {
LoadBalancerProperties properties = context
.getBean(LoadBalancerProperties.class);
return new HealthCheckServiceInstanceListSupplier(delegate,
properties.getHealthCheck(), webClient);
};
this.creators.add(creator);
return this;
}
/**
* Adds a {@link ZonePreferenceServiceInstanceListSupplier} to the
* {@link ServiceInstanceListSupplier} hierarchy.
* @return the {@link Builder} object
*/
public Builder withZonePreference() {
DelegateCreator creator = (context, delegate) -> {
LoadBalancerZoneConfig zoneConfig = context
.getBean(LoadBalancerZoneConfig.class);
return new ZonePreferenceServiceInstanceListSupplier(delegate,
zoneConfig);
};
this.creators.add(creator);
return this;
}
/**
* If {@link LoadBalancerCacheManager} is available in the context, wraps created
* {@link ServiceInstanceListSupplier} hierarchy with a
* {@link CachingServiceInstanceListSupplier} instance to provide a caching
* mechanism for service instances. Uses {@link ObjectProvider} to lazily resolve
* {@link LoadBalancerCacheManager}.
* @return the {@link Builder} object
*/
public Builder withCaching() {
if (cachingCreator != null && LOG.isWarnEnabled()) {
LOG.warn(
"Overriding a previously set cachingCreator with a CachingServiceInstanceListSupplier-based cachingCreator.");
}
this.cachingCreator = (context, delegate) -> {
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(delegate,
cacheManagerProvider.getIfAvailable());
}
if (LOG.isWarnEnabled()) {
LOG.warn(
"LoadBalancerCacheManager not available, returning delegate without caching.");
}
return delegate;
};
return this;
}
/**
* Builds the {@link ServiceInstanceListSupplier} hierarchy.
* @param context application context
* @return a {@link ServiceInstanceListSupplier} instance on top of the delegate
* hierarchy
*/
public ServiceInstanceListSupplier build(ConfigurableApplicationContext context) {
Assert.notNull(baseCreator, "A baseCreator must not be null");
ServiceInstanceListSupplier supplier = baseCreator.apply(context);
for (DelegateCreator creator : creators) {
supplier = creator.apply(context, supplier);
}
if (this.cachingCreator != null) {
supplier = this.cachingCreator.apply(context, supplier);
}
return supplier;
}
}
}

View File

@@ -36,30 +36,23 @@ import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig;
* @since 2.2.1
*/
public class ZonePreferenceServiceInstanceListSupplier
implements ServiceInstanceListSupplier {
extends DelegatingServiceInstanceListSupplier {
private final String ZONE = "zone";
private final ServiceInstanceListSupplier delegate;
private final LoadBalancerZoneConfig zoneConfig;
private String zone;
public ZonePreferenceServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
LoadBalancerZoneConfig zoneConfig) {
this.delegate = delegate;
super(delegate);
this.zoneConfig = zoneConfig;
}
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get().map(this::filteredByZone);
return getDelegate().get().map(this::filteredByZone);
}
private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) {

View File

@@ -0,0 +1,117 @@
/*
* 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.loadbalancer.core;
import org.junit.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
public class ServiceInstanceListSuppliersTests {
@Test
public void testBuilder() {
new ApplicationContextRunner().withUserConfiguration(CacheTestConfig.class)
.run(context -> {
ServiceInstanceListSupplier supplier = ServiceInstanceListSuppliers
.builder().withDiscoveryClient().withHealthChecks()
.withCaching().build(context);
assertThat(supplier)
.isInstanceOf(CachingServiceInstanceListSupplier.class);
DelegatingServiceInstanceListSupplier delegating = (DelegatingServiceInstanceListSupplier) supplier;
assertThat(delegating.getDelegate())
.isInstanceOf(HealthCheckServiceInstanceListSupplier.class);
delegating = (DelegatingServiceInstanceListSupplier) delegating
.getDelegate();
assertThat(delegating.getDelegate()).isInstanceOf(
DiscoveryClientServiceInstanceListSupplier.class);
});
}
@Test
public void testIllegalArgumentExceptionThrownWhenBaseBuilderNull() {
new ApplicationContextRunner().withUserConfiguration(CacheTestConfig.class)
.run(context -> {
try {
ServiceInstanceListSuppliers.builder().withHealthChecks()
.build(context);
fail("Should have thrown exception.");
}
catch (Exception exception) {
assertThat(exception)
.isInstanceOf(IllegalArgumentException.class);
}
});
}
@Test
public void testDelegateReturnedIfLoadBalancerCacheManagerNotAvailable() {
new ApplicationContextRunner().withUserConfiguration(BaseTestConfig.class)
.run(context -> {
ServiceInstanceListSupplier supplier = ServiceInstanceListSuppliers
.builder().withDiscoveryClient().withHealthChecks()
.withCaching().build(context);
assertThat(supplier)
.isNotInstanceOf(CachingServiceInstanceListSupplier.class);
assertThat(supplier)
.isInstanceOf(HealthCheckServiceInstanceListSupplier.class);
DelegatingServiceInstanceListSupplier delegating = (DelegatingServiceInstanceListSupplier) supplier;
assertThat(delegating.getDelegate()).isInstanceOf(
DiscoveryClientServiceInstanceListSupplier.class);
});
}
@Import(BaseTestConfig.class)
private static class CacheTestConfig {
@Bean
public LoadBalancerCacheManager cacheManager() {
return mock(LoadBalancerCacheManager.class);
}
}
private static class BaseTestConfig {
@Bean
public ReactiveDiscoveryClient reactiveDiscoveryClient() {
return mock(ReactiveDiscoveryClient.class);
}
@Bean
public LoadBalancerProperties loadBalancerProperties() {
return new LoadBalancerProperties();
}
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
}