From 65201f8f6113bfdb88f0c216e59503439a98241a Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Fri, 11 Dec 2020 10:33:42 -0600 Subject: [PATCH] Add RandomLoadBalancer, along with tests and docs. (#868) # Conflicts: # spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RoundRobinLoadBalancer.java --- .../main/asciidoc/spring-cloud-commons.adoc | 26 +++- .../core/NoopServiceInstanceListSupplier.java | 3 +- .../loadbalancer/core/RandomLoadBalancer.java | 121 ++++++++++++++++++ .../core/RandomLoadBalancerTests.java | 81 ++++++++++++ 4 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RandomLoadBalancer.java create mode 100644 spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RandomLoadBalancerTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index 6066295c..ceba4ef7 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -854,11 +854,33 @@ of compatible Spring Boot versions. == Spring Cloud LoadBalancer Spring Cloud provides its own client-side load-balancer abstraction and implementation. For the load-balancing -mechanism, `ReactiveLoadBalancer` interface has been added and a Round-Robin-based implementation -has been provided for it. In order to get instances to select from reactive `ServiceInstanceListSupplier` +mechanism, `ReactiveLoadBalancer` interface has been added and a *Round-Robin-based* and *Random* implementations +have been provided for it. In order to get instances to select from reactive `ServiceInstanceListSupplier` is used. Currently we support a service-discovery-based implementation of `ServiceInstanceListSupplier` that retrieves available instances from Service Discovery using a <> available in the classpath. +=== Switching between the load-balancing algorithms + +The `ReactiveLoadBalancer` implementation that is used by default is `RoundRobinLoadBalancer`. To switch to a different implementation, either for selected services or all of them, you can use the <>. + +For example, the following configuration can be passed via `@LoadBalancerClient` annotation to switch to using the `RandomLoadBalancer`: + +[[random-loadbalancer-configuration]] +[source,java,indent=0] +---- +public class CustomLoadBalancerConfiguration { + + @Bean + ReactorLoadBalancer randomLoadBalancer(Environment environment, + LoadBalancerClientFactory loadBalancerClientFactory) { + String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); + return new RandomLoadBalancer(loadBalancerClientFactory + .getLazyProvider(name, ServiceInstanceListSupplier.class), + name); + } +} +---- + === Spring Cloud LoadBalancer integrations In order to make it easy to use Spring Cloud LoadBalancer, we provide `ReactorLoadBalancerExchangeFilterFunction` that can be used with `WebClient` and `BlockingLoadBalancerClient` that works with `RestTemplate`. diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/NoopServiceInstanceListSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/NoopServiceInstanceListSupplier.java index d188862d..bfd0a952 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/NoopServiceInstanceListSupplier.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/NoopServiceInstanceListSupplier.java @@ -16,6 +16,7 @@ package org.springframework.cloud.loadbalancer.core; +import java.util.Collections; import java.util.List; import reactor.core.publisher.Flux; @@ -36,7 +37,7 @@ public class NoopServiceInstanceListSupplier implements ServiceInstanceListSuppl @Override public Flux> get() { - return Flux.empty(); + return Flux.defer(() -> Flux.just(Collections.emptyList())); } } diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RandomLoadBalancer.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RandomLoadBalancer.java new file mode 100644 index 00000000..597c813d --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RandomLoadBalancer.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-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.List; +import java.util.concurrent.ThreadLocalRandom; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.reactive.DefaultResponse; +import org.springframework.cloud.client.loadbalancer.reactive.EmptyResponse; +import org.springframework.cloud.client.loadbalancer.reactive.Request; +import org.springframework.cloud.client.loadbalancer.reactive.Response; + +/** + * A random-based implementation of {@link ReactorServiceInstanceLoadBalancer}. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.7 + */ +public class RandomLoadBalancer implements ReactorServiceInstanceLoadBalancer { + + private static final Log log = LogFactory.getLog(RandomLoadBalancer.class); + + private final String serviceId; + + @Deprecated + private ObjectProvider serviceInstanceSupplier; + + private ObjectProvider serviceInstanceListSupplierProvider; + + /** + * @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 RandomLoadBalancer( + ObjectProvider serviceInstanceListSupplierProvider, + String serviceId) { + this.serviceId = serviceId; + this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; + } + + /** + * @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 #RandomLoadBalancer(ObjectProvider, String)}} instead. + */ + @Deprecated + public RandomLoadBalancer(String serviceId, + ObjectProvider serviceInstanceSupplier) { + this.serviceId = serviceId; + this.serviceInstanceSupplier = serviceInstanceSupplier; + } + + @SuppressWarnings("rawtypes") + @Override + public Mono> choose( + Request request) { + // TODO: move supplier to Request? + // Temporary conditional logic till deprecated members are removed. + if (serviceInstanceListSupplierProvider != null) { + ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider + .getIfAvailable(NoopServiceInstanceListSupplier::new); + return supplier.get().next() + .map(serviceInstances -> processInstanceResponse(supplier, + serviceInstances)); + } + ServiceInstanceSupplier supplier = this.serviceInstanceSupplier + .getIfAvailable(NoopServiceInstanceSupplier::new); + return supplier.get().collectList().map(this::getInstanceResponse); + } + + private org.springframework.cloud.client.loadbalancer.reactive.Response processInstanceResponse( + ServiceInstanceListSupplier supplier, + List serviceInstances) { + org.springframework.cloud.client.loadbalancer.reactive.Response serviceInstanceResponse = getInstanceResponse( + serviceInstances); + if (supplier instanceof SelectedInstanceCallback + && serviceInstanceResponse.hasServer()) { + ((SelectedInstanceCallback) supplier) + .selectedServiceInstance(serviceInstanceResponse.getServer()); + } + return serviceInstanceResponse; + } + + private Response getInstanceResponse( + List instances) { + if (instances.isEmpty()) { + if (log.isWarnEnabled()) { + log.warn("No servers available for service: " + serviceId); + } + return new EmptyResponse(); + } + int index = ThreadLocalRandom.current().nextInt(instances.size()); + + ServiceInstance instance = instances.get(index); + + return new DefaultResponse(instance); + } + +} diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RandomLoadBalancerTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RandomLoadBalancerTests.java new file mode 100644 index 00000000..35ce612d --- /dev/null +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RandomLoadBalancerTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-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.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.Response; +import org.springframework.cloud.loadbalancer.support.SimpleObjectProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link RandomLoadBalancer}. + * + * @author Olga Maciaszek-Sharma + */ +class RandomLoadBalancerTests { + + private final ServiceInstance serviceInstance = new DefaultServiceInstance(); + + private RandomLoadBalancer loadBalancer; + + @Test + void shouldReturnOneServiceInstance() { + DiscoveryClientServiceInstanceListSupplier supplier = mock( + DiscoveryClientServiceInstanceListSupplier.class); + when(supplier.get()).thenReturn( + Flux.just(Arrays.asList(serviceInstance, new DefaultServiceInstance()))); + loadBalancer = new RandomLoadBalancer(new SimpleObjectProvider<>(supplier), + "test"); + + Response response = loadBalancer.choose().block(); + + assertThat(response.hasServer()).isTrue(); + } + + @Test + void shouldReturnEmptyResponseWhenSupplierNotAvailable() { + loadBalancer = new RandomLoadBalancer(new SimpleObjectProvider<>(null), "test"); + + Response response = loadBalancer.choose().block(); + + assertThat(response.hasServer()).isFalse(); + } + + @Test + void shouldReturnEmptyResponseWhenNoInstancesAvailable() { + DiscoveryClientServiceInstanceListSupplier supplier = mock( + DiscoveryClientServiceInstanceListSupplier.class); + when(supplier.get()).thenReturn(Flux.just(Collections.emptyList())); + loadBalancer = new RandomLoadBalancer(new SimpleObjectProvider<>(supplier), + "test"); + + Response response = loadBalancer.choose().block(); + + assertThat(response.hasServer()).isFalse(); + } + +}