Gh 595 add serviceinstancelistsupplier (#607)

* Add ServiceInstanceListSupplier and its implementations.
Deprecate ServiceInstanceSupplier and its implementations.
Fixes gh-595.

* Switch to non-parameterised type.

* Switch autoConfiguration and tests to new implementation.

* Update docs and javadocs. Remove deprecated TODOs.

* Fix after code review.
This commit is contained in:
Olga Maciaszek-Sharma
2019-09-18 17:56:52 +02:00
committed by GitHub
parent 1f6a4b1b37
commit da0ce3c4c8
16 changed files with 442 additions and 26 deletions

View File

@@ -418,7 +418,7 @@ will be handled by a non-reactive `LoadBalancerClient` under the hood. Additiona
spring-cloud-starter-netflix-ribbon is already in maintenance mode, so we do not recommend
adding it to new projects.
IMPORTANT: In order to make use of the more efficient cached version of `ServiceInstanceSupplier`,
IMPORTANT: 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

View File

@@ -24,10 +24,13 @@ import org.springframework.cache.CacheManager;
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
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.ServiceInstanceSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
@@ -43,13 +46,27 @@ import org.springframework.core.env.Environment;
@ConditionalOnDiscoveryEnabled
public class LoadBalancerClientConfiguration {
@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
DiscoveryClient discoveryClient, Environment env,
ObjectProvider<CacheManager> cacheManager) {
DiscoveryClientServiceInstanceListSupplier delegate = new DiscoveryClientServiceInstanceListSupplier(
discoveryClient, env);
if (cacheManager.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(delegate,
cacheManager.getIfAvailable());
}
return delegate;
}
@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
public ServiceInstanceSupplier discoveryClientServiceInstanceSupplier(
DiscoveryClient discoveryClient, Environment env,
ObjectProvider<CacheManager> cacheManager) {
// TODO: bean post processor to enable caching?
DiscoveryClientServiceInstanceSupplier delegate = new DiscoveryClientServiceInstanceSupplier(
discoveryClient, env);
if (cacheManager.getIfAvailable() != null) {
@@ -65,8 +82,8 @@ public class LoadBalancerClientConfiguration {
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(name, loadBalancerClientFactory
.getLazyProvider(name, ServiceInstanceSupplier.class));
return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class), name);
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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 java.util.List;
import java.util.Objects;
import reactor.cache.CacheFlux;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cloud.client.ServiceInstance;
/**
* A {@link ServiceInstanceListSupplier} implementation that tries retrieving
* {@link ServiceInstance} objects from cache; if none found, retrieves instances using
* {@link DiscoveryClientServiceInstanceListSupplier}.
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
public class CachingServiceInstanceListSupplier implements ServiceInstanceListSupplier {
/**
* Name of the service cache instance.
*/
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;
this.serviceInstances = CacheFlux.lookup(key -> {
// TODO: configurable cache name
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
List<ServiceInstance> list = Objects.requireNonNull(cache).get(key,
List.class);
if (list == null || list.isEmpty()) {
return Mono.empty();
}
return Flux.just(list).materialize().collectList();
}, delegate.getServiceId()).onCacheMissResume(this.delegate)
.andWriteWith((key, signals) -> Flux.fromIterable(signals).dematerialize()
.doOnNext(instances -> {
Cache cache = cacheManager
.getCache(SERVICE_INSTANCE_CACHE_NAME);
Objects.requireNonNull(cache).put(key, instances);
}).then());
}
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get() {
return serviceInstances;
}
}

View File

@@ -27,8 +27,10 @@ import org.springframework.cache.CacheManager;
import org.springframework.cloud.client.ServiceInstance;
/**
* @deprecated Use {@link CachingServiceInstanceListSupplier} instead.
* @author Spencer Gibb
*/
@Deprecated
public class CachingServiceInstanceSupplier implements ServiceInstanceSupplier {
/**
@@ -46,10 +48,8 @@ public class CachingServiceInstanceSupplier implements ServiceInstanceSupplier {
CacheManager cacheManager) {
this.delegate = delegate;
this.serviceInstances = CacheFlux.lookup(key -> {
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME); // TODO:
// configurable
// cache
// name
// TODO: configurable cache name
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
List<ServiceInstance> list = cache.get(key, List.class);
if (list == null || list.isEmpty()) {
return Mono.empty();

View File

@@ -0,0 +1,59 @@
/*
* 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 java.util.List;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.env.Environment;
import static org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory.PROPERTY_NAME;
/**
* A discovery-client-based {@link ServiceInstanceListSupplier} implementation.
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
public class DiscoveryClientServiceInstanceListSupplier
implements ServiceInstanceListSupplier {
private final DiscoveryClient delegate;
private final String serviceId;
public DiscoveryClientServiceInstanceListSupplier(DiscoveryClient delegate,
Environment environment) {
this.delegate = delegate;
this.serviceId = environment.getProperty(PROPERTY_NAME);
}
@Override
public String getServiceId() {
return serviceId;
}
@Override
public Flux<List<ServiceInstance>> get() {
return Flux.defer(() -> Flux.just(delegate.getInstances(serviceId)));
}
}

View File

@@ -27,8 +27,10 @@ import org.springframework.core.env.Environment;
import static org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory.PROPERTY_NAME;
/**
* @deprecated Use {@link DiscoveryClientServiceInstanceListSupplier} instead.
* @author Spencer Gibb
*/
@Deprecated
public class DiscoveryClientServiceInstanceSupplier implements ServiceInstanceSupplier {
private final DiscoveryClient delegate;

View File

@@ -0,0 +1,42 @@
/*
* 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 java.util.List;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
/**
* A no-op implementation of {@link ServiceInstanceListSupplier}.
*
* @author Olga Maciaszek-Sharma
*/
public class NoopServiceInstanceListSupplier implements ServiceInstanceListSupplier {
@Override
public String getServiceId() {
return "";
}
@Override
public Flux<List<ServiceInstance>> get() {
return Flux.empty();
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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 reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
/**
* A no-op implementation of {@link ServiceInstanceSupplier}.
*
* @author Olga Maciaszek-Sharma
* @deprecated Use {@link NoopServiceInstanceListSupplier} instead.
*/
@Deprecated
public class NoopServiceInstanceSupplier implements ServiceInstanceSupplier {
@Override
public String getServiceId() {
return "";
}
@Override
public Flux<ServiceInstance> get() {
return Flux.empty();
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.loadbalancer.core;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
@@ -31,7 +32,10 @@ import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
/**
* A Round-Robin-based implementation of {@link ReactorServiceInstanceLoadBalancer}.
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
*/
public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
@@ -39,15 +43,59 @@ public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalance
private final AtomicInteger position;
private final ObjectProvider<ServiceInstanceSupplier> serviceInstanceSupplier;
@Deprecated
private ObjectProvider<ServiceInstanceSupplier> serviceInstanceSupplier;
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
/**
* @param serviceId id of the service for which to choose an instance
* @param serviceInstanceSupplier a provider of {@link ServiceInstanceSupplier} that
* will be used to get available instances
* @deprecated Use {@link #RoundRobinLoadBalancer(ObjectProvider, String)}} instead.
*/
@Deprecated
public RoundRobinLoadBalancer(String serviceId,
ObjectProvider<ServiceInstanceSupplier> serviceInstanceSupplier) {
this(serviceId, serviceInstanceSupplier, new Random().nextInt(1000));
}
/**
* @param serviceInstanceListSupplierProvider a provider of
* {@link ServiceInstanceListSupplier} that will be used to get available instances
* @param serviceId id of the service for which to choose an instance
*/
public RoundRobinLoadBalancer(
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId) {
this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(1000));
}
/**
* @param serviceInstanceListSupplierProvider a provider of
* {@link ServiceInstanceListSupplier} that will be used to get available instances
* @param serviceId id of the service for which to choose an instance
* @param seedPosition Round Robin element position marker
*/
public RoundRobinLoadBalancer(
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId, int seedPosition) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.position = new AtomicInteger(seedPosition);
}
/**
* @param serviceId id of the service for which to choose an instance
* @param serviceInstanceSupplier a provider of {@link ServiceInstanceSupplier} that
* will be used to get available instances
* @param seedPosition Round Robin element position marker
* @deprecated Use {@link #RoundRobinLoadBalancer(ObjectProvider, String, int)}}
* instead.
*/
@Deprecated
public RoundRobinLoadBalancer(String serviceId,
ObjectProvider<ServiceInstanceSupplier> serviceInstanceSupplier,
int seedPosition) {
@@ -62,19 +110,29 @@ public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalance
// src/main/java/netflix/ocelli/loadbalancer/RoundRobinLoadBalancer.java
public Mono<Response<ServiceInstance>> choose(Request request) {
// TODO: move supplier to Request?
ServiceInstanceSupplier supplier = this.serviceInstanceSupplier.getIfAvailable();
return supplier.get().collectList().map(instances -> {
if (instances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
// TODO: enforce order?
int pos = Math.abs(this.position.incrementAndGet());
// Temporary conditional logic till deprecated members are removed.
if (serviceInstanceListSupplierProvider != null) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get().next().map(this::getInstanceResponse);
}
ServiceInstanceSupplier supplier = this.serviceInstanceSupplier
.getIfAvailable(NoopServiceInstanceSupplier::new);
return supplier.get().collectList().map(this::getInstanceResponse);
}
ServiceInstance instance = instances.get(pos % instances.size());
private Response<ServiceInstance> getInstanceResponse(
List<ServiceInstance> instances) {
if (instances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
// TODO: enforce order?
int pos = Math.abs(this.position.incrementAndGet());
return new DefaultResponse(instance);
});
ServiceInstance instance = instances.get(pos % instances.size());
return new DefaultResponse(instance);
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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 java.util.List;
import java.util.function.Supplier;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
/**
* A {@link Supplier} of lists of {@link ServiceInstance} objects.
*
* @author Olga Maciaszek-Sharma
* @since 2.2.0
*/
public interface ServiceInstanceListSupplier
extends Supplier<Flux<List<ServiceInstance>>> {
String getServiceId();
}

View File

@@ -24,7 +24,9 @@ import org.springframework.cloud.client.ServiceInstance;
/**
* @author Spencer Gibb
* @deprecated Use {@link ServiceInstanceListSupplier} instead.
*/
@Deprecated
public interface ServiceInstanceSupplier extends Supplier<Flux<ServiceInstance>> {
String getServiceId();

View File

@@ -0,0 +1,60 @@
/*
* 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.support;
import java.util.Arrays;
import java.util.List;
import reactor.core.publisher.Flux;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
/**
* Utility class for service instances.
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
*/
public final class ServiceInstanceListSuppliers {
private ServiceInstanceListSuppliers() {
throw new IllegalStateException("Can't instantiate a utility class");
}
public static ServiceInstanceListSupplier from(String serviceId,
ServiceInstance... instances) {
return new ServiceInstanceListSupplier() {
@Override
public Flux<List<ServiceInstance>> get() {
return Flux.just(Arrays.asList(instances));
}
@Override
public String getServiceId() {
return serviceId;
}
};
}
public static ObjectProvider<ServiceInstanceListSupplier> toProvider(String serviceId,
ServiceInstance... instances) {
return new SimpleObjectProvider<>(from(serviceId, instances));
}
}

View File

@@ -26,7 +26,9 @@ import org.springframework.cloud.loadbalancer.core.ServiceInstanceSupplier;
* Utility class for service instances.
*
* @author Spencer Gibb
* @deprecated Use {@link ServiceInstanceListSuppliers} instead.
*/
@Deprecated
public final class ServiceInstanceSuppliers {
private ServiceInstanceSuppliers() {

View File

@@ -96,7 +96,7 @@ class BlockingLoadBalancerClientTests {
}
@Test
void exceptionThrownIfInstanceNotAvailableForRequestExecution() throws IOException {
void exceptionThrownIfInstanceNotAvailableForRequestExecution() {
try {
final String result = "result";
Object actualResult = loadBalancerClient.execute("unknownservice",

View File

@@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class BlockingLoadBalancerClientAutoConfigurationTests {
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
private ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.cloud.loadbalancer.ribbon.enabled=false",
"debug=true")
.withConfiguration(AutoConfigurations.of(LoadBalancerAutoConfiguration.class,

View File

@@ -38,6 +38,7 @@ import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceSuppliers;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
@@ -51,7 +52,7 @@ import static org.assertj.core.api.BDDAssertions.then;
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class LoadBalancerTest {
public class LoadBalancerTests {
@Autowired
private LoadBalancerClientFactory clientFactory;
@@ -124,9 +125,20 @@ public class LoadBalancerTest {
assertLoadBalancer(loadBalancer, Arrays.asList("1host", "2host-secure"));
}
@Test
public void staticConfigurationWorksWithServiceInstanceListSupplier() {
String serviceId = "test1";
RoundRobinLoadBalancer loadBalancer = new RoundRobinLoadBalancer(
ServiceInstanceListSuppliers.toProvider(serviceId,
instance(serviceId, "1host", false),
instance(serviceId, "2host-secure", true)),
serviceId, -1);
assertLoadBalancer(loadBalancer, Arrays.asList("1host", "2host-secure"));
}
private DefaultServiceInstance instance(String serviceId, String host,
boolean secure) {
return new DefaultServiceInstance(serviceId, host, 80, secure);
return new DefaultServiceInstance(serviceId, serviceId, host, 80, secure);
}
@EnableAutoConfiguration
@@ -147,8 +159,8 @@ public class LoadBalancerTest {
public RoundRobinLoadBalancer roundRobinContextLoadBalancer(
LoadBalancerClientFactory clientFactory, Environment env) {
String serviceId = clientFactory.getName(env);
return new RoundRobinLoadBalancer(serviceId, clientFactory
.getLazyProvider(serviceId, ServiceInstanceSupplier.class), -1);
return new RoundRobinLoadBalancer(clientFactory.getLazyProvider(serviceId,
ServiceInstanceListSupplier.class), serviceId, -1);
}
}