Merge remote-tracking branch 'origin/2.2.x'

# Conflicts:
#	docs/src/main/asciidoc/spring-cloud-commons.adoc
#	spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java
#	spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RoundRobinLoadBalancer.java
#	spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerTests.java
This commit is contained in:
Olga Maciaszek-Sharma
2020-12-04 18:57:33 +01:00
8 changed files with 289 additions and 1 deletions

View File

@@ -970,6 +970,29 @@ public class CustomLoadBalancerConfiguration {
WARNING: `HealthCheckServiceInstanceListSupplier` has its own caching mechanism based on Reactor Flux `replay()`. Therefore, if it's being used, you may want to skip wrapping that supplier with `CachingServiceInstanceListSupplier`.
=== Same instance preference for LoadBalancer
You can set up the LoadBalancer in such a way that it prefers the instance that was previously selected, if that instance is available.
For that, you need to use `SameInstancePreferenceServiceInstanceListSupplier`. You can configure it either by setting the value of `spring.cloud.loadbalancer.configurations` to `same-instance-preference` or by providing your own `ServiceInstanceListSupplier` bean -- for example:
[source,java,indent=0]
----
public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withSameInstancePreference()
.build(context);
}
}
----
TIP: This is also a replacement for Zookeeper `StickyRule`.
[[spring-cloud-loadbalancer-hints]]
=== Spring Cloud LoadBalancer Hints

View File

@@ -98,6 +98,17 @@ public class LoadBalancerClientConfiguration {
return ServiceInstanceListSupplier.builder().withDiscoveryClient().withHealthChecks().build(context);
}
@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations",
havingValue = "same-instance-preference")
public ServiceInstanceListSupplier sameInstancePreferenceServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withDiscoveryClient().withSameInstancePreference()
.build(context);
}
}
@Configuration(proxyBeanMethods = false)
@@ -135,6 +146,17 @@ public class LoadBalancerClientConfiguration {
.build(context);
}
@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations",
havingValue = "same-instance-preference")
public ServiceInstanceListSupplier sameInstancePreferenceServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withSameInstancePreference()
.build(context);
}
}
@Configuration(proxyBeanMethods = false)

View File

@@ -78,7 +78,17 @@ public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalance
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map(this::getInstanceResponse);
return supplier.get(request).next()
.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
}
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
List<ServiceInstance> serviceInstances) {
Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {

View File

@@ -0,0 +1,80 @@
/*
* 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.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
/**
* An implementation of {@link ServiceInstanceListSupplier} that selects the previously
* chosen instance if it's available.
*
* @author Olga Maciaszek-Sharma
* @since 3.0.0
*/
public class SameInstancePreferenceServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier
implements SelectedInstanceCallback {
private static final Log LOG = LogFactory.getLog(SameInstancePreferenceServiceInstanceListSupplier.class);
private ServiceInstance previouslyReturnedInstance;
public SameInstancePreferenceServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
super(delegate);
}
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get().map(this::filteredBySameInstancePreference);
}
private List<ServiceInstance> filteredBySameInstancePreference(List<ServiceInstance> serviceInstances) {
if (previouslyReturnedInstance != null && serviceInstances.contains(previouslyReturnedInstance)) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Returning previously selected service instance: %s",
previouslyReturnedInstance));
}
return Collections.singletonList(previouslyReturnedInstance);
}
if (LOG.isDebugEnabled()) {
LOG.debug(String.format(
"Previously selected service instance %s was not available. Returning all the instances returned by delegate.",
previouslyReturnedInstance));
}
previouslyReturnedInstance = null;
return serviceInstances;
}
@Override
public void selectedServiceInstance(ServiceInstance serviceInstance) {
if (previouslyReturnedInstance == null || !previouslyReturnedInstance.equals(serviceInstance)) {
previouslyReturnedInstance = serviceInstance;
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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 org.springframework.cloud.client.ServiceInstance;
/**
* A callback interface that allows to pass the selected service instance data from the
* LoadBalancer.
*
* @author Olga Maciaszek-Sharma
*/
public interface SelectedInstanceCallback {
/**
* Passes the selected {@link ServiceInstance} as an argument.
* @param serviceInstance that has been selected
*/
void selectedServiceInstance(ServiceInstance serviceInstance);
}

View File

@@ -116,6 +116,18 @@ public final class ServiceInstanceListSupplierBuilder {
return this;
}
/**
* Adds a {@link SameInstancePreferenceServiceInstanceListSupplier} to the
* {@link ServiceInstanceListSupplier} hierarchy.
* @return the {@link ServiceInstanceListSupplierBuilder} object
*/
public ServiceInstanceListSupplierBuilder withSameInstancePreference() {
DelegateCreator creator = (context,
delegate) -> new SameInstancePreferenceServiceInstanceListSupplier(delegate);
this.creators.add(creator);
return this;
}
/**
* Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided
* {@link WebClient} instance to the {@link ServiceInstanceListSupplier} hierarchy.

View File

@@ -17,10 +17,12 @@
package org.springframework.cloud.loadbalancer.core;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -43,6 +45,7 @@ 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.SimpleObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.Environment;
@@ -50,6 +53,9 @@ import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.BDDAssertions.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Spencer Gibb
@@ -139,6 +145,21 @@ public class LoadBalancerTests {
assertThat(serviceInstance.getServiceId()).isEqualTo("test2");
}
@Test
public void selectedInstanceCallback() {
String serviceId = "test1";
ServiceInstance serviceInstance = instance(serviceId, "1host", false);
SameInstancePreferenceServiceInstanceListSupplier supplier = mock(
SameInstancePreferenceServiceInstanceListSupplier.class);
when(supplier.get()).thenReturn(Flux.just(Collections.singletonList(serviceInstance)));
RoundRobinLoadBalancer loadBalancer = new RoundRobinLoadBalancer(new SimpleObjectProvider<>(supplier),
serviceId);
loadBalancer.choose().block();
verify(supplier).selectedServiceInstance(serviceInstance);
}
private static class TestHintLoadBalancer extends RoundRobinLoadBalancer {
TestHintLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,

View File

@@ -0,0 +1,85 @@
/*
* 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.List;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Tests for {@link SameInstancePreferenceServiceInstanceListSupplier}.
*
* @author Olga Maciaszek-Sharma
*/
class SameInstancePreferenceServiceInstanceListSupplierTests {
private final DiscoveryClientServiceInstanceListSupplier delegate = mock(
DiscoveryClientServiceInstanceListSupplier.class);
private final SameInstancePreferenceServiceInstanceListSupplier supplier = new SameInstancePreferenceServiceInstanceListSupplier(
delegate);
private final ServiceInstance first = serviceInstance("test-1");
private final ServiceInstance second = serviceInstance("test-2");
private final ServiceInstance third = serviceInstance("test-3");
@Test
void shouldReturnPreviouslySelectedInstanceIfAvailable() {
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(first, second, third)));
supplier.selectedServiceInstance(first);
List<ServiceInstance> instances = supplier.get().blockFirst();
assertThat(instances).hasSize(1);
assertThat(instances.get(0)).isEqualTo(first);
}
@Test
void shouldReturnAllInstancesFromDelegateIfNoPreviouslySelectedInstance() {
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(first, second, third)));
List<ServiceInstance> instances = supplier.get().blockFirst();
assertThat(instances).hasSize(3);
}
@Test
void shouldReturnAllInstancesFromDelegateIfPreviouslySelectedInstanceIfAvailable() {
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(second, third)));
supplier.selectedServiceInstance(first);
List<ServiceInstance> instances = supplier.get().blockFirst();
assertThat(instances).hasSize(2);
}
private DefaultServiceInstance serviceInstance(String instanceId) {
return new DefaultServiceInstance(instanceId, "test", "http://test.test", 9080, false);
}
}