Avoid retrying on same instance (#834)

* cherry-pick switching to properties

* Pass information on previous ServiceInstance to RequestContext.

# Conflicts:
#	spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/retry/BlockingLoadBalancedRetryPolicy.java

* Add a RoundRobinLoadBalancer implementation that avoids same service instance while retrying.

* Wrap instances in ArrayList. Add tests.

* Enable AvoidPreviousInstanceRoundRobinLoadBalancer by default if SpringRetry on classpath.

* Fix failing tests. Add javadocs and author tags.

* Fix properties.

* Add documentation.

* Fix docs after review.

* Fix docs after review.

* Handle avoiding previous instance with ServiceInstanceListSupplier in place of LoadBalancer.

* Fix property name.

* Change spelling.
This commit is contained in:
Olga Maciaszek-Sharma
2020-09-29 12:36:54 -05:00
committed by GitHub
parent cbe4bb1139
commit 25c17082ee
26 changed files with 521 additions and 220 deletions

View File

@@ -35,6 +35,7 @@
|spring.cloud.loadbalancer.health-check.interval | 25s | Interval for rerunning the HealthCheck scheduler.
|spring.cloud.loadbalancer.health-check.path | |
|spring.cloud.loadbalancer.hint | | Allows setting the value of <code>hint</code> that is passed on to the LoadBalancer request and can subsequently be used in {@link ReactiveLoadBalancer} implementations.
|spring.cloud.loadbalancer.retry.avoid-previous-instance | true | Enables wrapping ServiceInstanceListSupplier beans with `RetryAwareServiceInstanceListSupplier` if Spring-Retry is in the classpath.
|spring.cloud.loadbalancer.retry.enabled | true |
|spring.cloud.loadbalancer.retry.max-retries-on-next-service-instance | 1 | Number of retries to be executed on the next <code>ServiceInstance</code>. A <code>ServiceInstance</code> is chosen before each retry call.
|spring.cloud.loadbalancer.retry.max-retries-on-same-service-instance | 0 | Number of retries to be executed on the same <code>ServiceInstance</code>.

View File

@@ -448,6 +448,9 @@ You can set:
- `spring.cloud.loadbalancer.retry.maxRetriesOnSameServiceInstance` - indicates how many times a request should be retried on the same `ServiceInstance` (counted separately for every selected instance)
- `spring.cloud.loadbalancer.retry.maxRetriesOnNextServiceInstance` - indicates how many times a request should be retried a newly selected `ServiceInstance`
- `spring.cloud.loadbalancer.retry.retryableStatusCodes` - the status codes on which to always retry a failed request.
NOTE: For load-balanced retries, by default, we wrap the `ServiceInstanceListSupplier` bean with `RetryAwareServiceInstanceListSupplier` to select a different instance from the one previously chosen, if available. You can disable this behavior by setting the value of `spring.cloud.loadbalancer.retry.avoidPreviousInstance` to `false`.
====
[source,java,indent=0]
----

View File

@@ -25,6 +25,7 @@ import org.springframework.retry.RetryPolicy;
* requests.
*
* @author Ryan Baxter
* @author Olga Maciaszek-Sharma
*/
public class InterceptorRetryPolicy implements RetryPolicy {
@@ -56,7 +57,7 @@ public class InterceptorRetryPolicy implements RetryPolicy {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
if (lbContext.getRetryCount() == 0 && lbContext.getServiceInstance() == null) {
// We haven't even tried to make the request yet so return true so we do
lbContext.setServiceInstance(serviceInstanceChooser.choose(serviceName));
lbContext.setServiceInstance(null);
return true;
}
return policy.canRetryNextServer(lbContext);

View File

@@ -25,6 +25,7 @@ import org.springframework.retry.context.RetryContextSupport;
* {@link RetryContext} for load-balanced retries.
*
* @author Ryan Baxter
* @author Olga Maciaszek-Sharma
*/
public class LoadBalancedRetryContext extends RetryContextSupport {
@@ -32,6 +33,8 @@ public class LoadBalancedRetryContext extends RetryContextSupport {
private ServiceInstance serviceInstance;
private ServiceInstance previousServiceInstance;
/**
* Creates a new load-balanced context.
* @param parent The parent context.
@@ -71,7 +74,16 @@ public class LoadBalancedRetryContext extends RetryContextSupport {
* @param serviceInstance The service instance to use during the retry.
*/
public void setServiceInstance(ServiceInstance serviceInstance) {
setPreviousServiceInstance(this.serviceInstance);
this.serviceInstance = serviceInstance;
}
public ServiceInstance getPreviousServiceInstance() {
return previousServiceInstance;
}
public void setPreviousServiceInstance(ServiceInstance previousServiceInstance) {
this.previousServiceInstance = previousServiceInstance;
}
}

View File

@@ -44,11 +44,12 @@ import org.springframework.web.client.RestTemplate;
* @author Dave Syer
* @author Will Tran
* @author Gang Li
* @author Olga Maciaszek-Sharma
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
@EnableConfigurationProperties(LoadBalancerProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@@ -125,11 +126,11 @@ public class LoadBalancerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RetryLoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient,
LoadBalancerRetryProperties retryProperties, LoadBalancerRequestFactory requestFactory,
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
LoadBalancerProperties properties, LoadBalancerRequestFactory requestFactory,
LoadBalancedRetryFactory loadBalancedRetryFactory,
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
return new RetryLoadBalancerInterceptor(loadBalancerClient, retryProperties, requestFactory,
loadBalancedRetryFactory, properties, loadBalancerFactory);
return new RetryLoadBalancerInterceptor(loadBalancerClient, properties, requestFactory,
loadBalancedRetryFactory, loadBalancerFactory);
}
@Bean

View File

@@ -1,105 +0,0 @@
/*
* 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.client.loadbalancer;
import java.util.HashSet;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpMethod;
/**
* Configuration properties for the {@link LoadBalancerClient}.
*
* @author Ryan Baxter
*/
@ConfigurationProperties("spring.cloud.loadbalancer.retry")
public class LoadBalancerRetryProperties {
private boolean enabled = true;
/**
* Indicates retries should be attempted on operations other than
* {@link HttpMethod#GET}.
*/
private boolean retryOnAllOperations = false;
/**
* Number of retries to be executed on the same <code>ServiceInstance</code>.
*/
private int maxRetriesOnSameServiceInstance = 0;
/**
* Number of retries to be executed on the next <code>ServiceInstance</code>. A
* <code>ServiceInstance</code> is chosen before each retry call.
*/
private int maxRetriesOnNextServiceInstance = 1;
/**
* A {@link Set} of status codes that should trigger a retry.
*/
private Set<Integer> retryableStatusCodes = new HashSet<>();
/**
* Returns true if the load balancer should retry failed requests.
* @return True if the load balancer should retry failed requests; false otherwise.
*/
public boolean isEnabled() {
return this.enabled;
}
/**
* Sets whether the load balancer should retry failed requests.
* @param enabled Whether the load balancer should retry failed requests.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isRetryOnAllOperations() {
return retryOnAllOperations;
}
public void setRetryOnAllOperations(boolean retryOnAllOperations) {
this.retryOnAllOperations = retryOnAllOperations;
}
public int getMaxRetriesOnSameServiceInstance() {
return maxRetriesOnSameServiceInstance;
}
public void setMaxRetriesOnSameServiceInstance(int maxRetriesOnSameServiceInstance) {
this.maxRetriesOnSameServiceInstance = maxRetriesOnSameServiceInstance;
}
public int getMaxRetriesOnNextServiceInstance() {
return maxRetriesOnNextServiceInstance;
}
public void setMaxRetriesOnNextServiceInstance(int maxRetriesOnNextServiceInstance) {
this.maxRetriesOnNextServiceInstance = maxRetriesOnNextServiceInstance;
}
public Set<Integer> getRetryableStatusCodes() {
return retryableStatusCodes;
}
public void setRetryableStatusCodes(Set<Integer> retryableStatusCodes) {
this.retryableStatusCodes = retryableStatusCodes;
}
}

View File

@@ -52,18 +52,15 @@ public class RetryLoadBalancerInterceptor implements ClientHttpRequestIntercepto
private final LoadBalancedRetryFactory lbRetryFactory;
private final LoadBalancerRetryProperties retryProperties;
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory;
public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRetryProperties retryProperties,
public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerProperties properties,
LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory lbRetryFactory,
LoadBalancerProperties properties, ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
this.loadBalancer = loadBalancer;
this.retryProperties = retryProperties;
this.properties = properties;
this.requestFactory = requestFactory;
this.lbRetryFactory = lbRetryFactory;
this.properties = properties;
this.loadBalancerFactory = loadBalancerFactory;
}
@@ -85,11 +82,21 @@ public class RetryLoadBalancerInterceptor implements ClientHttpRequestIntercepto
.getSupportedLifecycleProcessors(
loadBalancerFactory.getInstances(serviceName, LoadBalancerLifecycle.class),
HttpRequestContext.class, ClientHttpResponse.class, ServiceInstance.class);
String hint = getHint(serviceName);
DefaultRequest<HttpRequestContext> lbRequest = new DefaultRequest<>(new HttpRequestContext(request, hint));
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
if (serviceInstance == null) {
ServiceInstance previousServiceInstance = null;
if (context instanceof LoadBalancedRetryContext) {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
previousServiceInstance = lbContext.getPreviousServiceInstance();
}
String hint = getHint(serviceName);
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(
new RetryableRequestContext(previousServiceInstance, request, hint));
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
serviceInstance = loadBalancer.choose(serviceName, lbRequest);
if (context instanceof LoadBalancedRetryContext) {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
lbContext.setServiceInstance(serviceInstance);
}
}
Response<ServiceInstance> lbResponse = new DefaultResponse(serviceInstance);
if (serviceInstance == null) {
@@ -127,7 +134,7 @@ public class RetryLoadBalancerInterceptor implements ClientHttpRequestIntercepto
if (retryListeners != null && retryListeners.length != 0) {
template.setListeners(retryListeners);
}
template.setRetryPolicy(!retryProperties.isEnabled() || retryPolicy == null ? new NeverRetryPolicy()
template.setRetryPolicy(!properties.getRetry().isEnabled() || retryPolicy == null ? new NeverRetryPolicy()
: new InterceptorRetryPolicy(request, retryPolicy, loadBalancer, serviceName));
return template;
}

View File

@@ -0,0 +1,49 @@
/*
* 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.client.loadbalancer;
import org.springframework.cloud.client.ServiceInstance;
/**
* A request context object that allows storing information on previously used service
* instances.
*
* @author Olga Maciaszek-Sharma
*/
public class RetryableRequestContext extends DefaultRequestContext {
private final ServiceInstance previousServiceInstance;
public RetryableRequestContext(ServiceInstance previousServiceInstance) {
this.previousServiceInstance = previousServiceInstance;
}
public RetryableRequestContext(ServiceInstance previousServiceInstance, Object clientRequest) {
super(clientRequest);
this.previousServiceInstance = previousServiceInstance;
}
public RetryableRequestContext(ServiceInstance previousServiceInstance, Object clientRequest, String hint) {
super(clientRequest, hint);
this.previousServiceInstance = previousServiceInstance;
}
public ServiceInstance getPreviousServiceInstance() {
return previousServiceInstance;
}
}

View File

@@ -17,9 +17,12 @@
package org.springframework.cloud.client.loadbalancer.reactive;
import java.time.Duration;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpMethod;
import org.springframework.util.LinkedCaseInsensitiveMap;
/**
@@ -43,6 +46,11 @@ public class LoadBalancerProperties {
*/
private Map<String, String> hint = new LinkedCaseInsensitiveMap<>();
/**
* Properties for Spring-Retry support in Spring Cloud LoadBalancer.
*/
private Retry retry = new Retry();
public HealthCheck getHealthCheck() {
return healthCheck;
}
@@ -59,6 +67,14 @@ public class LoadBalancerProperties {
this.hint = hint;
}
public Retry getRetry() {
return retry;
}
public void setRetry(Retry retry) {
this.retry = retry;
}
public static class HealthCheck {
/**
@@ -99,4 +115,81 @@ public class LoadBalancerProperties {
}
public static class Retry {
private boolean enabled = true;
/**
* Indicates retries should be attempted on operations other than
* {@link HttpMethod#GET}.
*/
private boolean retryOnAllOperations = false;
/**
* Number of retries to be executed on the same <code>ServiceInstance</code>.
*/
private int maxRetriesOnSameServiceInstance = 0;
/**
* Number of retries to be executed on the next <code>ServiceInstance</code>. A
* <code>ServiceInstance</code> is chosen before each retry call.
*/
private int maxRetriesOnNextServiceInstance = 1;
/**
* A {@link Set} of status codes that should trigger a retry.
*/
private Set<Integer> retryableStatusCodes = new HashSet<>();
/**
* Returns true if the load balancer should retry failed requests.
* @return True if the load balancer should retry failed requests; false
* otherwise.
*/
public boolean isEnabled() {
return this.enabled;
}
/**
* Sets whether the load balancer should retry failed requests.
* @param enabled Whether the load balancer should retry failed requests.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isRetryOnAllOperations() {
return retryOnAllOperations;
}
public void setRetryOnAllOperations(boolean retryOnAllOperations) {
this.retryOnAllOperations = retryOnAllOperations;
}
public int getMaxRetriesOnSameServiceInstance() {
return maxRetriesOnSameServiceInstance;
}
public void setMaxRetriesOnSameServiceInstance(int maxRetriesOnSameServiceInstance) {
this.maxRetriesOnSameServiceInstance = maxRetriesOnSameServiceInstance;
}
public int getMaxRetriesOnNextServiceInstance() {
return maxRetriesOnNextServiceInstance;
}
public void setMaxRetriesOnNextServiceInstance(int maxRetriesOnNextServiceInstance) {
this.maxRetriesOnNextServiceInstance = maxRetriesOnNextServiceInstance;
}
public Set<Integer> getRetryableStatusCodes() {
return retryableStatusCodes;
}
public void setRetryableStatusCodes(Set<Integer> retryableStatusCodes) {
this.retryableStatusCodes = retryableStatusCodes;
}
}
}

View File

@@ -28,7 +28,6 @@ import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -100,11 +99,6 @@ public abstract class AbstractLoadBalancerAutoConfigurationTests {
return new NoopLoadBalancerClient();
}
@Bean
LoadBalancerProperties loadBalancerProperties() {
return new LoadBalancerProperties();
}
@Bean
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory() {
return new TestLoadBalancerFactory();

View File

@@ -22,7 +22,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.http.HttpRequest;
import org.springframework.retry.RetryContext;
@@ -35,6 +34,7 @@ import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
* @author Olga Maciaszek-Sharma
*/
@RunWith(MockitoJUnitRunner.class)
public class InterceptorRetryPolicyTest {
@@ -69,10 +69,8 @@ public class InterceptorRetryPolicyTest {
serviceInstanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
when(context.getRetryCount()).thenReturn(0);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(serviceInstanceChooser.choose(eq(serviceName))).thenReturn(serviceInstance);
then(interceptorRetryPolicy.canRetry(context)).isTrue();
verify(context, times(1)).setServiceInstance(eq(serviceInstance));
verify(context, times(1)).setServiceInstance(eq(null));
}

View File

@@ -26,7 +26,6 @@ import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -163,11 +162,6 @@ public class LoadBalancerRequestFactoryConfigurationTests {
return mock(LoadBalancerClient.class);
}
@Bean
LoadBalancerProperties loadBalancerProperties() {
return new LoadBalancerProperties();
}
@SuppressWarnings("unchecked")
@Bean
ReactiveLoadBalancer.Factory<ServiceInstance> factory() {

View File

@@ -74,8 +74,6 @@ public class RetryLoadBalancerInterceptorTests {
private LoadBalancerClient client;
private LoadBalancerRetryProperties retryProperties;
private LoadBalancerRequestFactory lbRequestFactory;
private final LoadBalancedRetryFactory loadBalancedRetryFactory = new LoadBalancedRetryFactory() {
@@ -88,7 +86,6 @@ public class RetryLoadBalancerInterceptorTests {
@Before
public void setUp() {
client = mock(LoadBalancerClient.class);
retryProperties = new LoadBalancerRetryProperties();
lbRequestFactory = mock(LoadBalancerRequestFactory.class);
properties = new LoadBalancerProperties();
lbFactory = mock(ReactiveLoadBalancer.Factory.class);
@@ -97,7 +94,6 @@ public class RetryLoadBalancerInterceptorTests {
@After
public void tearDown() {
client = null;
retryProperties = null;
}
@Test(expected = IOException.class)
@@ -108,9 +104,9 @@ public class RetryLoadBalancerInterceptorTests {
when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
.thenThrow(new IOException());
retryProperties.setEnabled(false);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, loadBalancedRetryFactory, properties, lbFactory);
properties.getRetry().setEnabled(false);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, loadBalancedRetryFactory, lbFactory);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
@@ -124,9 +120,9 @@ public class RetryLoadBalancerInterceptorTests {
public void interceptInvalidHost() throws Throwable {
HttpRequest request = mock(HttpRequest.class);
when(request.getURI()).thenReturn(new URI("http://foo_underscore"));
retryProperties.setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, loadBalancedRetryFactory, properties, lbFactory);
properties.getRetry().setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, loadBalancedRetryFactory, lbFactory);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
interceptor.intercept(request, body, execution);
@@ -142,9 +138,9 @@ public class RetryLoadBalancerInterceptorTests {
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
.thenReturn(clientHttpResponse);
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
retryProperties.setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, loadBalancedRetryFactory, properties, lbFactory);
properties.getRetry().setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, loadBalancedRetryFactory, lbFactory);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
interceptor.intercept(request, body, execution);
@@ -162,9 +158,9 @@ public class RetryLoadBalancerInterceptorTests {
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
.thenReturn(clientHttpResponse);
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
retryProperties.setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy), properties, lbFactory);
properties.getRetry().setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -188,9 +184,9 @@ public class RetryLoadBalancerInterceptorTests {
when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
when(client.execute(eq("foo"), eq(serviceInstance), nullable(LoadBalancerRequest.class)))
.thenReturn(clientHttpResponseNotFound).thenReturn(clientHttpResponseOk);
retryProperties.setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy), properties, lbFactory);
properties.getRetry().setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -219,11 +215,11 @@ public class RetryLoadBalancerInterceptorTests {
ArgumentMatchers.<LoadBalancerRequest<ClientHttpResponse>>any()))
.thenReturn(clientHttpResponseNotFound);
retryProperties.setEnabled(true);
properties.getRetry().setEnabled(true);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy), properties, lbFactory);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory);
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
verify(client, times(1)).execute(eq("foo"), eq(serviceInstance),
@@ -251,9 +247,9 @@ public class RetryLoadBalancerInterceptorTests {
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
.thenThrow(new IOException()).thenReturn(clientHttpResponse);
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
retryProperties.setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy), properties, lbFactory);
properties.getRetry().setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -275,9 +271,9 @@ public class RetryLoadBalancerInterceptorTests {
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
.thenThrow(new IOException()).thenReturn(clientHttpResponse);
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
retryProperties.setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy), properties, lbFactory);
properties.getRetry().setEnabled(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
interceptor.intercept(request, body, execution);
@@ -300,13 +296,13 @@ public class RetryLoadBalancerInterceptorTests {
when(client.choose(eq("listener"), any())).thenReturn(serviceInstance);
when(client.execute(eq("listener"), eq(serviceInstance), any(LoadBalancerRequest.class)))
.thenThrow(new IOException()).thenReturn(clientHttpResponse);
retryProperties.setEnabled(true);
properties.getRetry().setEnabled(true);
MyRetryListener retryListener = new MyRetryListener();
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory,
new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { retryListener }),
properties, lbFactory);
lbFactory);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -329,10 +325,10 @@ public class RetryLoadBalancerInterceptorTests {
when(client.choose(eq("default"), any())).thenReturn(serviceInstance);
when(client.execute(eq("default"), eq(serviceInstance), any(LoadBalancerRequest.class)))
.thenThrow(new IOException()).thenReturn(clientHttpResponse);
retryProperties.setEnabled(true);
properties.getRetry().setEnabled(true);
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy), properties, lbFactory);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory);
byte[] body = new byte[] {};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -348,17 +344,17 @@ public class RetryLoadBalancerInterceptorTests {
when(request.getURI()).thenReturn(new URI("http://noRetry"));
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
retryProperties.setEnabled(true);
properties.getRetry().setEnabled(true);
RetryListener myRetryListener = new RetryListenerSupport() {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
return false;
}
};
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory,
new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { myRetryListener }),
properties, lbFactory);
lbFactory);
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
interceptor.intercept(request, new byte[] {}, execution);
}
@@ -372,8 +368,8 @@ public class RetryLoadBalancerInterceptorTests {
HttpRequest request = mock(HttpRequest.class);
when(request.getURI()).thenReturn(new URI("http://test"));
TestLoadBalancerClient client = new TestLoadBalancerClient();
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryProperties,
lbRequestFactory, loadBalancedRetryFactory, properties, lbFactory);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties,
lbRequestFactory, loadBalancedRetryFactory, lbFactory);
interceptor.intercept(request, new byte[] {}, mock(ClientHttpRequestExecution.class));

View File

@@ -16,7 +16,10 @@
package org.springframework.cloud.loadbalancer.annotation;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled;
@@ -26,14 +29,18 @@ 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.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.RetryAwareServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.retry.support.RetryTemplate;
/**
* @author Spencer Gibb
@@ -129,4 +136,42 @@ public class LoadBalancerClientConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBlockingDiscoveryEnabled
@ConditionalOnClass(RetryTemplate.class)
@Conditional(OnAvoidPreviousInstanceAndRetryEnabledCondition.class)
@AutoConfigureAfter(BlockingSupportConfiguration.class)
@ConditionalOnBean(ServiceInstanceListSupplier.class)
public static class BlockingRetryConfiguration {
@Bean
@ConditionalOnBean(DiscoveryClient.class)
@Primary
public ServiceInstanceListSupplier retryAwareDiscoveryClientServiceInstanceListSupplier(
ServiceInstanceListSupplier delegate) {
return new RetryAwareServiceInstanceListSupplier(delegate);
}
}
static final class OnAvoidPreviousInstanceAndRetryEnabledCondition extends AllNestedConditions {
private OnAvoidPreviousInstanceAndRetryEnabledCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
matchIfMissing = true)
static class LoadBalancerRetryEnabled {
}
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.avoid-previous-instance", havingValue = "true",
matchIfMissing = true)
static class AvoidPreviousInstanceEnabled {
}
}
}

View File

@@ -18,8 +18,8 @@ package org.springframework.cloud.loadbalancer.blocking.retry;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRetryProperties;
import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
/**
@@ -31,15 +31,15 @@ import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalanc
*/
public class BlockingLoadBalancedRetryFactory implements LoadBalancedRetryFactory {
private final LoadBalancerRetryProperties retryProperties;
private final LoadBalancerProperties loadBalancerProperties;
public BlockingLoadBalancedRetryFactory(LoadBalancerRetryProperties retryProperties) {
this.retryProperties = retryProperties;
public BlockingLoadBalancedRetryFactory(LoadBalancerProperties loadBalancerProperties) {
this.loadBalancerProperties = loadBalancerProperties;
}
@Override
public LoadBalancedRetryPolicy createRetryPolicy(String serviceId, ServiceInstanceChooser serviceInstanceChooser) {
return new BlockingLoadBalancedRetryPolicy(serviceId, serviceInstanceChooser, retryProperties);
return new BlockingLoadBalancedRetryPolicy(loadBalancerProperties);
}
}

View File

@@ -18,8 +18,7 @@ package org.springframework.cloud.loadbalancer.blocking.retry;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRetryProperties;
import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.http.HttpMethod;
@@ -33,37 +32,30 @@ import org.springframework.http.HttpMethod;
*/
public class BlockingLoadBalancedRetryPolicy implements LoadBalancedRetryPolicy {
private final LoadBalancerRetryProperties retryProperties;
private final ServiceInstanceChooser serviceInstanceChooser;
private final String serviceId;
private final LoadBalancerProperties properties;
private int sameServerCount = 0;
private int nextServerCount = 0;
public BlockingLoadBalancedRetryPolicy(String serviceId, ServiceInstanceChooser serviceInstanceChooser,
LoadBalancerRetryProperties retryProperties) {
this.serviceId = serviceId;
this.serviceInstanceChooser = serviceInstanceChooser;
this.retryProperties = retryProperties;
public BlockingLoadBalancedRetryPolicy(LoadBalancerProperties properties) {
this.properties = properties;
}
public boolean canRetry(LoadBalancedRetryContext context) {
HttpMethod method = context.getRequest().getMethod();
return HttpMethod.GET.equals(method) || retryProperties.isRetryOnAllOperations();
return HttpMethod.GET.equals(method) || properties.getRetry().isRetryOnAllOperations();
}
@Override
public boolean canRetrySameServer(LoadBalancedRetryContext context) {
return sameServerCount < retryProperties.getMaxRetriesOnSameServiceInstance() && canRetry(context);
return sameServerCount < properties.getRetry().getMaxRetriesOnSameServiceInstance() && canRetry(context);
}
@Override
public boolean canRetryNextServer(LoadBalancedRetryContext context) {
// After the failure, we increment first and then check, hence the equality check
return nextServerCount <= retryProperties.getMaxRetriesOnNextServiceInstance() && canRetry(context);
return nextServerCount <= properties.getRetry().getMaxRetriesOnNextServiceInstance() && canRetry(context);
}
@Override
@@ -81,7 +73,10 @@ public class BlockingLoadBalancedRetryPolicy implements LoadBalancedRetryPolicy
context.setExhaustedOnly();
}
else {
context.setServiceInstance(serviceInstanceChooser.choose(serviceId));
// We want the service instance to be set by
// `RetryLoadBalancerInterceptor`
// in order to get the entire data of the request
context.setServiceInstance(null);
}
}
else {
@@ -91,7 +86,7 @@ public class BlockingLoadBalancedRetryPolicy implements LoadBalancedRetryPolicy
@Override
public boolean retryableStatusCode(int statusCode) {
return retryProperties.getRetryableStatusCodes().contains(statusCode);
return properties.getRetry().getRetryableStatusCodes().contains(statusCode);
}
}

View File

@@ -25,7 +25,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRetryProperties;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
@@ -60,13 +59,13 @@ public class BlockingLoadBalancerClientAutoConfiguration {
@Configuration
@ConditionalOnClass(RetryTemplate.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
@EnableConfigurationProperties(LoadBalancerProperties.class)
protected static class BlockingLoadBalancerRetryConfig {
@Bean
@ConditionalOnMissingBean
LoadBalancedRetryFactory loadBalancedRetryFactory(LoadBalancerRetryProperties retryProperties) {
return new BlockingLoadBalancedRetryFactory(retryProperties);
LoadBalancedRetryFactory loadBalancedRetryFactory(LoadBalancerProperties properties) {
return new BlockingLoadBalancedRetryFactory(properties);
}
}

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.ArrayList;
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;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
/**
* A {@link ServiceInstanceListSupplier} implementation that avoids picking the same
* service instance while retrying requests.
*
* @author Olga Maciaszek-Sharma
* @since 3.0.0
*/
public class RetryAwareServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
private final Log LOG = LogFactory.getLog(RetryAwareServiceInstanceListSupplier.class);
public RetryAwareServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
super(delegate);
}
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get(Request request) {
if (!(request.getContext() instanceof RetryableRequestContext)) {
return get();
}
RetryableRequestContext context = (RetryableRequestContext) request.getContext();
ServiceInstance previousServiceInstance = context.getPreviousServiceInstance();
if (previousServiceInstance == null) {
return get();
}
return get().map(instances -> filteredByPreviousInstance(instances, previousServiceInstance));
}
private List<ServiceInstance> filteredByPreviousInstance(List<ServiceInstance> instances,
ServiceInstance previousServiceInstance) {
List<ServiceInstance> filteredInstances = new ArrayList<>(instances);
if (previousServiceInstance != null) {
filteredInstances.remove(previousServiceInstance);
}
if (filteredInstances.size() > 0) {
return filteredInstances;
}
if (LOG.isWarnEnabled()) {
LOG.warn(String.format(
"No instances found after removing previously used service instance from the search (%s). Returning all found instances.",
previousServiceInstance));
}
return instances;
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get();
}
}

View File

@@ -41,11 +41,11 @@ public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalance
private static final Log log = LogFactory.getLog(RoundRobinLoadBalancer.class);
private final AtomicInteger position;
final AtomicInteger position;
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
final String serviceId;
private final String serviceId;
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
/**
* @param serviceInstanceListSupplierProvider a provider of
@@ -78,12 +78,14 @@ public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalance
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get().next().map(this::getInstanceResponse);
return supplier.get(request).next().map(this::getInstanceResponse);
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
// TODO: enforce order?

View File

@@ -24,6 +24,7 @@ import reactor.core.publisher.Flux;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.core.env.Environment;
import static org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory.PROPERTY_NAME;
@@ -38,6 +39,10 @@ public interface ServiceInstanceListSupplier extends Supplier<Flux<List<ServiceI
String getServiceId();
default Flux<List<ServiceInstance>> get(Request request) {
return get();
}
static ServiceInstanceListSupplierBuilder builder() {
return new ServiceInstanceListSupplierBuilder();
}

View File

@@ -172,6 +172,12 @@ public final class ServiceInstanceListSupplierBuilder {
return this;
}
public ServiceInstanceListSupplierBuilder withRetryAwareness() {
DelegateCreator creator = (context, delegate) -> new RetryAwareServiceInstanceListSupplier(delegate);
creators.add(creator);
return this;
}
/**
* Builds the {@link ServiceInstanceListSupplier} hierarchy.
* @param context application context

View File

@@ -26,7 +26,7 @@ import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
/**
* Utility class for service instances.
* Utility class for service instance list suppliers.
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma

View File

@@ -21,6 +21,12 @@
"name": "spring.cloud.loadbalancer.cache.enabled",
"description": "Enables Spring Cloud LoadBalancer caching mechanism.",
"type": "java.lang.Boolean"
},
{
"defaultValue": true,
"name": "spring.cloud.loadbalancer.retry.avoid-previous-instance",
"description": "Enables wrapping ServiceInstanceListSupplier beans with `RetryAwareServiceInstanceListSupplier` if Spring-Retry is in the classpath.",
"type": "java.lang.Boolean"
}
]
}

View File

@@ -19,6 +19,7 @@ package org.springframework.cloud.loadbalancer.annotation;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration;
import org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration;
@@ -29,10 +30,12 @@ import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSup
import org.springframework.cloud.loadbalancer.core.DelegatingServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.HealthCheckServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.RetryAwareServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ZonePreferenceServiceInstanceListSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.BDDAssertions.then;
@@ -50,6 +53,12 @@ class LoadBalancerClientConfigurationTests {
LoadBalancerClientConfiguration.class));
ApplicationContextRunner blockingDiscoveryClientRunner = new ApplicationContextRunner()
.withClassLoader(new FilteredClassLoader(RetryTemplate.class))
.withConfiguration(AutoConfigurations.of(CompositeDiscoveryClientAutoConfiguration.class,
LoadBalancerCacheAutoConfiguration.class, LoadBalancerAutoConfiguration.class,
LoadBalancerClientConfiguration.class));
ApplicationContextRunner blockingDiscoveryClientRunnerWithRetry = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CompositeDiscoveryClientAutoConfiguration.class,
LoadBalancerCacheAutoConfiguration.class, LoadBalancerAutoConfiguration.class,
LoadBalancerClientConfiguration.class));
@@ -126,6 +135,30 @@ class LoadBalancerClientConfigurationTests {
});
}
@Test
void shouldWrapWithRetryAwareSupplierWhenRetryTemplateOnClasspath() {
blockingDiscoveryClientRunnerWithRetry.run(context -> {
ServiceInstanceListSupplier supplier = context.getBean(ServiceInstanceListSupplier.class);
then(supplier).isInstanceOf(RetryAwareServiceInstanceListSupplier.class);
then(((DelegatingServiceInstanceListSupplier) supplier).getDelegate())
.isInstanceOf(CachingServiceInstanceListSupplier.class);
then(((DelegatingServiceInstanceListSupplier) ((DelegatingServiceInstanceListSupplier) supplier)
.getDelegate()).getDelegate()).isInstanceOf(DiscoveryClientServiceInstanceListSupplier.class);
});
}
@Test
void shouldNotWrapWithRetryAwareSupplierWhenRetryTemplateOnClasspath() {
blockingDiscoveryClientRunner.withPropertyValues("spring.cloud.loadbalancer.retry.avoidPreviousInstance=false")
.run(context -> {
ServiceInstanceListSupplier supplier = context.getBean(ServiceInstanceListSupplier.class);
then(supplier).isInstanceOf(CachingServiceInstanceListSupplier.class);
then(((DelegatingServiceInstanceListSupplier) supplier).getDelegate())
.isInstanceOf(DiscoveryClientServiceInstanceListSupplier.class);
});
}
@Configuration
protected static class TestConfig {

View File

@@ -23,7 +23,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRetryProperties;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
@@ -47,7 +47,7 @@ class BlockingLoadBalancedRetryPolicyTests {
private final LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
private final LoadBalancerRetryProperties retryProperties = new LoadBalancerRetryProperties();
private final LoadBalancerProperties properties = new LoadBalancerProperties();
private final UnsupportedOperationException exception = new UnsupportedOperationException();
@@ -59,8 +59,8 @@ class BlockingLoadBalancedRetryPolicyTests {
@Test
void shouldExecuteIndicatedNumberOfSameAndNextInstanceRetriesAndCloseRetryContext() {
retryProperties.setMaxRetriesOnSameServiceInstance(1);
BlockingLoadBalancedRetryPolicy retryPolicy = getRetryPolicy(retryProperties);
properties.getRetry().setMaxRetriesOnSameServiceInstance(1);
BlockingLoadBalancedRetryPolicy retryPolicy = getRetryPolicy(properties);
assertThat(retryPolicy.canRetrySameServer(context)).isTrue();
assertThat(retryPolicy.canRetryNextServer(context)).isTrue();
@@ -88,7 +88,7 @@ class BlockingLoadBalancedRetryPolicyTests {
void shouldNotRetryWhenMethodNotGet() {
when(httpRequest.getMethod()).thenReturn(HttpMethod.POST);
when(context.getRequest()).thenReturn(httpRequest);
BlockingLoadBalancedRetryPolicy retryPolicy = getRetryPolicy(retryProperties);
BlockingLoadBalancedRetryPolicy retryPolicy = getRetryPolicy(properties);
boolean canRetry = retryPolicy.canRetry(context);
@@ -99,8 +99,8 @@ class BlockingLoadBalancedRetryPolicyTests {
void shouldRetryOnPostWhenEnabled() {
when(httpRequest.getMethod()).thenReturn(HttpMethod.POST);
when(context.getRequest()).thenReturn(httpRequest);
retryProperties.setRetryOnAllOperations(true);
BlockingLoadBalancedRetryPolicy retryPolicy = getRetryPolicy(retryProperties);
properties.getRetry().setRetryOnAllOperations(true);
BlockingLoadBalancedRetryPolicy retryPolicy = getRetryPolicy(properties);
boolean canRetry = retryPolicy.canRetry(context);
@@ -109,16 +109,16 @@ class BlockingLoadBalancedRetryPolicyTests {
@Test
void shouldResolveRetryableStatusCode() {
retryProperties.setRetryableStatusCodes(new HashSet<>(Arrays.asList(404, 502)));
BlockingLoadBalancedRetryPolicy retryPolicy = getRetryPolicy(retryProperties);
properties.getRetry().setRetryableStatusCodes(new HashSet<>(Arrays.asList(404, 502)));
BlockingLoadBalancedRetryPolicy retryPolicy = getRetryPolicy(properties);
boolean retryableStatusCode = retryPolicy.retryableStatusCode(404);
assertThat(retryableStatusCode).isTrue();
}
private BlockingLoadBalancedRetryPolicy getRetryPolicy(LoadBalancerRetryProperties retryProperties) {
return new BlockingLoadBalancedRetryPolicy("test", loadBalancerClient, retryProperties);
private BlockingLoadBalancedRetryPolicy getRetryPolicy(LoadBalancerProperties properties) {
return new BlockingLoadBalancedRetryPolicy(properties);
}
}

View File

@@ -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.List;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RetryAwareServiceInstanceListSupplier}.
*
* @author Olga Maciaszek-Sharma
*/
class RetryAwareServiceInstanceListSupplierTests {
private final String serviceId = "test";
private static DefaultServiceInstance instance(String serviceId, String host, boolean secure) {
return new DefaultServiceInstance(serviceId, serviceId, host, 80, secure);
}
@Test
void shouldReturnEmptyListIfNoInstances() {
ServiceInstanceListSupplier delegate = ServiceInstanceListSuppliers.from(serviceId);
ServiceInstanceListSupplier supplier = new RetryAwareServiceInstanceListSupplier(delegate);
List<ServiceInstance> returnedInstances = supplier.get(new DefaultRequest<>(new RetryableRequestContext(null)))
.blockFirst();
assertThat(returnedInstances).isEmpty();
}
@Test
void shouldReturnFilteredInstances() {
ServiceInstance firstInstance = instance(serviceId, "1host", false);
ServiceInstance secondInstance = instance(serviceId, "2host-secure", true);
ServiceInstanceListSupplier delegate = ServiceInstanceListSuppliers.from(serviceId, firstInstance,
secondInstance);
ServiceInstanceListSupplier supplier = new RetryAwareServiceInstanceListSupplier(delegate);
List<ServiceInstance> returnedInstances = supplier
.get(new DefaultRequest<>(new RetryableRequestContext(firstInstance))).blockFirst();
assertThat(returnedInstances).containsExactly(secondInstance);
}
@Test
void shouldReturnAllInstancesIfFilteredInstancesEmpty() {
ServiceInstance firstInstance = instance(serviceId, "1host", false);
ServiceInstanceListSupplier delegate = ServiceInstanceListSuppliers.from(serviceId, firstInstance);
ServiceInstanceListSupplier supplier = new RetryAwareServiceInstanceListSupplier(delegate);
List<ServiceInstance> returnedInstances = supplier
.get(new DefaultRequest<>(new RetryableRequestContext(firstInstance))).blockFirst();
assertThat(returnedInstances).containsExactly(firstInstance);
}
}