Retry implementation for LoadBalanced RestTemplates

This commit is contained in:
Ryan Baxter
2016-09-23 15:00:15 -04:00
parent 329c67fa36
commit 19a7562c7c
20 changed files with 797 additions and 33 deletions

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>1.1.4.BUILD-SNAPSHOT</version>
<version>1.1.5.BUILD-SNAPSHOT</version>
</parent>
<packaging>pom</packaging>
<name>Spring Cloud Commons Docs</name>

View File

@@ -345,6 +345,20 @@ The Ribbon client is used to create a full physical address. See
{githubroot}/spring-cloud-netflix/blob/master/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java[RibbonAutoConfiguration]
for details of how the `RestTemplate` is set up.
==== Retrying Failed Requests
A load balanced `RestTemplate` can be configured to retry failed requests.
By default this logic is disabled, you can enable it by setting
`spring.cloud.loadbalancer.retry=true`. The load balanced `RestTemplate` will
honor some of the Ribbon configuration values related to retrying failed requests.
The properties you can use are `client.ribbon.MaxAutoRetries`,
`client.ribbon.MaxAutoRetriesNextServer`, and `client.ribbon.OkToRetryOnAllOperations`.
See the https://github.com/Netflix/ribbon/wiki/Getting-Started#the-properties-file-sample-clientproperties[Ribbon documentation]
for a description of what there properties do.
NOTE: `client` in the above examples should be replaced with your Ribbon client's
name.
=== Multiple RestTemplate objects
If you want a `RestTemplate` that is not load balanced, create a `RestTemplate`

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>1.1.4.BUILD-SNAPSHOT</version>
<version>1.1.5.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Cloud Commons Parent</name>
<description>Spring Cloud Commons Parent</description>

View File

@@ -9,7 +9,7 @@
<relativePath/>
</parent>
<artifactId>spring-cloud-commons-dependencies</artifactId>
<version>1.1.4.BUILD-SNAPSHOT</version>
<version>1.1.5.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-cloud-commons-dependencies</name>
<description>Spring Cloud Commons Dependencies</description>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>1.1.4.BUILD-SNAPSHOT</version>
<version>1.1.5.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-commons</artifactId>
@@ -53,6 +53,16 @@
<artifactId>spring-boot-starter-hateoas</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2013-2016 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
*
* http://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.http.HttpRequest;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryPolicy;
/**
* {@link RetryPolicy} used by the {@link LoadBalancerClient} when retrying failed requests.
* @author Ryan Baxter
*/
public class InterceptorRetryPolicy implements RetryPolicy {
private HttpRequest request;
private LoadBalancedRetryPolicy policy;
private LoadBalanceChooser loadBalanceChooser;
private String serviceName;
/**
* Creates a new retry policy.
* @param request the request that will be retried
* @param policy the retry policy from the load balancer
* @param loadBalanceChooser the load balancer client
* @param serviceName the name of the service
*/
public InterceptorRetryPolicy(HttpRequest request, LoadBalancedRetryPolicy policy,
LoadBalanceChooser loadBalanceChooser, String serviceName) {
this.request = request;
this.policy = policy;
this.loadBalanceChooser = loadBalanceChooser;
this.serviceName = serviceName;
}
@Override
public boolean canRetry(RetryContext context) {
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(loadBalanceChooser.choose(serviceName));
return true;
}
return policy.canRetryNextServer(lbContext);
}
@Override
public RetryContext open(RetryContext parent) {
return new LoadBalancedRetryContext(parent, request);
}
@Override
public void close(RetryContext context) {
policy.close((LoadBalancedRetryContext)context);
}
@Override
public void registerThrowable(RetryContext context, Throwable throwable) {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
//this is important as it registers the last exception in the context and also increases the retry count
lbContext.registerThrowable(throwable);
//let the policy know about the exception as well
policy.registerThrowable(lbContext, throwable);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
InterceptorRetryPolicy that = (InterceptorRetryPolicy) o;
if (!request.equals(that.request)) return false;
if (!policy.equals(that.policy)) return false;
if (!loadBalanceChooser.equals(that.loadBalanceChooser)) return false;
return serviceName.equals(that.serviceName);
}
@Override
public int hashCode() {
int result = request.hashCode();
result = 31 * result + policy.hashCode();
result = 31 * result + loadBalanceChooser.hashCode();
result = 31 * result + serviceName.hashCode();
return result;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2013-2016 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
*
* http://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;
/**
* Implemented by classes which use a load balancer to choose a server to
* send a request to.
*
* @author Ryan Baxter
*/
public interface LoadBalanceChooser {
/**
* Choose a ServiceInstance from the LoadBalancer for the specified service
* @param serviceId the service id to look up the LoadBalancer
* @return a ServiceInstance that matches the serviceId
*/
ServiceInstance choose(String serviceId);
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2013-2016 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
*
* http://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;
import org.springframework.http.HttpRequest;
import org.springframework.retry.RetryContext;
import org.springframework.retry.context.RetryContextSupport;
/**
* {@link RetryContext} for load balanced retries.
* @author Ryan Baxter
*/
public class LoadBalancedRetryContext extends RetryContextSupport {
private HttpRequest request;
private ServiceInstance serviceInstance;
/**
* Creates a new load balanced context.
* @param parent the parent context
* @param request the request that is being load balanced
*/
public LoadBalancedRetryContext(RetryContext parent, HttpRequest request) {
super(parent);
this.request = request;
}
/**
* Gets the request that is being load balanced.
* @return the request that is being load balanced
*/
public HttpRequest getRequest() {
return request;
}
/**
* Sets the request that is being load baalnced.
* @param request the request to load balanced
*/
public void setRequest(HttpRequest request) {
this.request = request;
}
/**
* Gets the service instance used during the retry.
* @return the service instance used during the retry
*/
public ServiceInstance getServiceInstance() {
return serviceInstance;
}
/**
* Sets the service instance to use during the retry.
* @param serviceInstance the service instance to use during the retry
*/
public void setServiceInstance(ServiceInstance serviceInstance) {
this.serviceInstance = serviceInstance;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2013-2016 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
*
* http://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;
/**
* Retry logic to use for the {@link LoadBalancerClient}.
* @author Ryan Baxter
*/
public interface LoadBalancedRetryPolicy {
/**
* Return true to retry the failed request on the same server.
* This method may be called more than once when executing a single operation.
* @param context the context for the retry operation
* @return true to retry the failed request on the same server, false otherwise
*/
public boolean canRetrySameServer(LoadBalancedRetryContext context);
/**
* Return true to retry the failed request on the next server from the load balancer.
* This method may be called more than once when executing a single operation.
* @param context the context for the retry operation
* @return true to retry the failed request on the next server from the load balancer, false otherwise
*/
public boolean canRetryNextServer(LoadBalancedRetryContext context);
/**
* Called when the retry operation has ended.
* @param context the context for the retry operation
*/
public abstract void close(LoadBalancedRetryContext context);
/**
* Called when the execution fails.
* @param context the context for the retry operation
* @param throwable the throwable from the failed execution.
*/
public abstract void registerThrowable(LoadBalancedRetryContext context, Throwable throwable);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2013-2016 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
*
* http://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;
/**
* Responsible for creating the {@link LoadBalancedRetryPolicy}.
* @author Ryan Baxter
*/
public interface LoadBalancedRetryPolicyFactory {
/**
* Creates a {@link LoadBalancedRetryPolicy}.
* @param serviceId The ID of the service to create the retry policy for.
* @param loadBalanceChooser Used to get the next server from a load balancer
* @return A retry policy for the service.
*/
public LoadBalancedRetryPolicy create(String serviceId, LoadBalanceChooser loadBalanceChooser);
static class NeverRetryFactory implements LoadBalancedRetryPolicyFactory {
@Override
public LoadBalancedRetryPolicy create(String serviceId, LoadBalanceChooser loadBalanceChooser) {
return null;
}
}
}

View File

@@ -25,9 +25,11 @@ import org.springframework.beans.factory.annotation.Autowired;
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.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestTemplate;
/**
@@ -39,6 +41,7 @@ import org.springframework.web.client.RestTemplate;
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@@ -76,9 +79,22 @@ public class LoadBalancerAutoConfiguration {
}
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerInterceptor(loadBalancerClient);
public RetryTemplate retryTemplate() {
RetryTemplate template = new RetryTemplate();
template.setThrowLastExceptionOnExhausted(true);
return template;
}
@Bean
@ConditionalOnMissingBean
public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() {
return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();
}
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties,
LoadBalancedRetryPolicyFactory lbRetryPolicyFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, retryTemplate(), properties, lbRetryPolicyFactory);
}
}

View File

@@ -16,22 +16,16 @@
package org.springframework.cloud.client.loadbalancer;
import org.springframework.cloud.client.ServiceInstance;
import java.io.IOException;
import java.net.URI;
import org.springframework.cloud.client.ServiceInstance;
/**
* Represents a client side load balancer
* @author Spencer Gibb
*/
public interface LoadBalancerClient {
/**
* Choose a ServiceInstance from the LoadBalancer for the specified service
* @param serviceId the service id to look up the LoadBalancer
* @return a ServiceInstance that matches the serviceId
*/
ServiceInstance choose(String serviceId);
public interface LoadBalancerClient extends LoadBalanceChooser {
/**
* execute request using a ServiceInstance from the LoadBalancer for the specified
@@ -44,6 +38,18 @@ public interface LoadBalancerClient {
*/
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
/**
* execute request using a ServiceInstance from the LoadBalancer for the specified
* service
* @param serviceId the service id to look up the LoadBalancer
* @param serviceInstance the service to execute the request to
* @param request allows implementations to execute pre and post actions such as
* incrementing metrics
* @return the result of the LoadBalancerRequest callback on the selected
* ServiceInstance
*/
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
/**
* Create a proper URI with a real host and port for systems to utilize.
* Some systems use a URI with the logical serivce name as the host,
@@ -54,5 +60,4 @@ public interface LoadBalancerClient {
* @return a reconstructed URI
*/
URI reconstructURI(ServiceInstance instance, URI original);
}

View File

@@ -16,44 +16,79 @@
package org.springframework.cloud.client.loadbalancer;
import java.io.IOException;
import java.net.URI;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import java.io.IOException;
import java.net.URI;
/**
* @author Spencer Gibb
* @author Dave Syer
* @author Ryan Baxter
*/
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private RetryTemplate retryTemplate;
private LoadBalancerRetryProperties lbProperties;
private LoadBalancedRetryPolicyFactory lbRetryPolicyFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, RetryTemplate retryTemplate, LoadBalancerRetryProperties lbProperties,
LoadBalancedRetryPolicyFactory lbRetryPolicyFactory) {
this.loadBalancer = loadBalancer;
this.retryTemplate = retryTemplate;
this.lbProperties = lbProperties;
this.lbRetryPolicyFactory = lbRetryPolicyFactory;
}
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
return this.loadBalancer.execute(serviceName,
new LoadBalancerRequest<ClientHttpResponse>() {
final String serviceName = originalUri.getHost();
LoadBalancedRetryPolicy retryPolicy = lbRetryPolicyFactory.create(serviceName,
loadBalancer);
retryTemplate.setRetryPolicy(
!lbProperties.isEnabled() || retryPolicy == null ? new NeverRetryPolicy()
: new InterceptorRetryPolicy(request, retryPolicy, loadBalancer,
serviceName));
return retryTemplate
.execute(new RetryCallback<ClientHttpResponse, IOException>() {
@Override
public ClientHttpResponse apply(final ServiceInstance instance)
throws Exception {
HttpRequest serviceRequest = new ServiceRequestWrapper(request,
instance);
return execution.execute(serviceRequest, body);
}
public ClientHttpResponse doWithRetry(RetryContext context)
throws IOException {
ServiceInstance serviceInstance = null;
if (context instanceof LoadBalancedRetryContext) {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
serviceInstance = lbContext.getServiceInstance();
}
if (serviceInstance == null) {
serviceInstance = loadBalancer.choose(serviceName);
}
return LoadBalancerInterceptor.this.loadBalancer.execute(
serviceName, serviceInstance,
new LoadBalancerRequest<ClientHttpResponse>() {
@Override
public ClientHttpResponse apply(
final ServiceInstance instance)
throws Exception {
HttpRequest serviceRequest = new ServiceRequestWrapper(
request, instance);
return execution.execute(serviceRequest, body);
}
});
}
});
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2013-2016 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
*
* http://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.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for the {@link LoadBalancerClient}.
* @author Ryan Baxter
*/
@ConfigurationProperties("spring.cloud.loadbalancer.retry")
public class LoadBalancerRetryProperties {
private boolean enabled = false;
/**
* Returns true if the load balancer should retry failed requests.
* @return true if the load balancer should retry failed request, false otherwise.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Sets whether the load balancer should retry failed request.
* @param enabled whether the load balancer should retry failed requests
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,115 @@
package org.springframework.cloud.client.loadbalancer;
import org.hamcrest.core.IsInstanceOf;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.http.HttpRequest;
import org.springframework.retry.RetryContext;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
*/
@RunWith(MockitoJUnitRunner.class)
public class InterceptorRetryPolicyTest {
private HttpRequest request;
private LoadBalancedRetryPolicy policy;
private LoadBalanceChooser loadBalanceChooser;
private String serviceName;
@Before
public void setup() {
request = mock(HttpRequest.class);
policy = mock(LoadBalancedRetryPolicy.class);
loadBalanceChooser = mock(LoadBalanceChooser.class);
serviceName = "foo";
}
@After
public void teardown() {
request = null;
policy = null;
loadBalanceChooser = null;
serviceName = null;
}
@Test
public void canRetryBeforeExecution() throws Exception {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, loadBalanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
when(context.getRetryCount()).thenReturn(0);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(loadBalanceChooser.choose(eq(serviceName))).thenReturn(serviceInstance);
assertThat(interceptorRetryPolicy.canRetry(context), is(true));
verify(context, times(1)).setServiceInstance(eq(serviceInstance));
}
@Test
public void canRetryNextServer() throws Exception {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, loadBalanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
when(context.getRetryCount()).thenReturn(1);
when(policy.canRetryNextServer(eq(context))).thenReturn(true);
assertThat(interceptorRetryPolicy.canRetry(context), is(true));
}
@Test
public void cannotRetry() throws Exception {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, loadBalanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
when(context.getRetryCount()).thenReturn(1);
assertThat(interceptorRetryPolicy.canRetry(context), is(false));
}
@Test
public void open() throws Exception {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, loadBalanceChooser, serviceName);
RetryContext context = interceptorRetryPolicy.open(null);
assertThat(context, IsInstanceOf.instanceOf(LoadBalancedRetryContext.class));
}
@Test
public void close() throws Exception {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, loadBalanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
interceptorRetryPolicy.close(context);
verify(policy, times(1)).close(eq(context));
}
@Test
public void registerThrowable() throws Exception {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, loadBalanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
Throwable thrown = new Exception();
when(policy.canRetryNextServer(eq(context))).thenReturn(true);
when(policy.canRetrySameServer(eq(context))).thenReturn(false);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(loadBalanceChooser.choose(eq(serviceName))).thenReturn(serviceInstance);
interceptorRetryPolicy.registerThrowable(context, thrown);
verify(context, times(1)).registerThrowable(eq(thrown));
verify(policy, times(1)).registerThrowable(eq(context), eq(thrown));
}
@Test
public void equals() throws Exception {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, loadBalanceChooser, serviceName);
assertThat(interceptorRetryPolicy.equals(null), is(false));
assertThat(interceptorRetryPolicy.equals(new Object()), is(false));
assertThat(interceptorRetryPolicy.equals(interceptorRetryPolicy), is(true));
assertThat(interceptorRetryPolicy.equals(new InterceptorRetryPolicy(request, policy, loadBalanceChooser, serviceName)), is(true));
}
}

View File

@@ -0,0 +1,58 @@
package org.springframework.cloud.client.loadbalancer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.http.HttpRequest;
import org.springframework.retry.RetryContext;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.mock;
/**
* @author Ryan Baxter
*/
@RunWith(MockitoJUnitRunner.class)
public class LoadBalancedRetryContextTest {
private RetryContext context;
private HttpRequest request;
@Before
public void setUp() throws Exception {
context = mock(RetryContext.class);
request = mock(HttpRequest.class);
}
@After
public void tearDown() throws Exception {
context = null;
request = null;
}
@Test
public void getRequest() throws Exception {
LoadBalancedRetryContext lbContext = new LoadBalancedRetryContext(context, request);
assertThat(lbContext.getRequest(), is(request));
}
@Test
public void setRequest() throws Exception {
LoadBalancedRetryContext lbContext = new LoadBalancedRetryContext(context, request);
HttpRequest newRequest = mock(HttpRequest.class);
lbContext.setRequest(newRequest);
assertThat(lbContext.getRequest(), is(newRequest));
}
@Test
public void getServiceInstance() throws Exception {
LoadBalancedRetryContext lbContext = new LoadBalancedRetryContext(context, request);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
lbContext.setServiceInstance(serviceInstance);
assertThat(lbContext.getServiceInstance(), is(serviceInstance));
}
}

View File

@@ -1,5 +1,6 @@
package org.springframework.cloud.client.loadbalancer;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.List;
@@ -16,6 +17,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.retry.RetryPolicy;
import org.springframework.web.client.RestTemplate;
import static org.hamcrest.Matchers.empty;
@@ -92,6 +94,9 @@ public class LoadBalancerAutoConfigurationTests {
return new NoopLoadBalancerClient();
}
@Bean
LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() { return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();}
}
@Configuration
@@ -114,6 +119,9 @@ public class LoadBalancerAutoConfigurationTests {
return new NoopLoadBalancerClient();
}
@Bean
LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() { return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();}
@Configuration
protected static class Two {
@Autowired
@@ -141,6 +149,12 @@ public class LoadBalancerAutoConfigurationTests {
return request.apply(choose(serviceId));
}
@Override
@SneakyThrows
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
return request.apply(choose(serviceId));
}
@Override
public URI reconstructURI(ServiceInstance instance, URI original) {
return DefaultServiceInstance.getUri(instance);

View File

@@ -0,0 +1,153 @@
package org.springframework.cloud.client.loadbalancer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import java.io.IOException;
import java.net.URI;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
*/
@RunWith(MockitoJUnitRunner.class)
public class LoadBalancerInterceptorTest {
private LoadBalancerClient client;
private RetryTemplate retryTemplate;
private LoadBalancerRetryProperties lbProperties;
@Before
public void setUp() throws Exception {
client = mock(LoadBalancerClient.class);
retryTemplate = spy(new RetryTemplate());
lbProperties = new LoadBalancerRetryProperties();
}
@After
public void tearDown() throws Exception {
client = null;
retryTemplate = null;
lbProperties = null;
}
@Test(expected = IOException.class)
public void interceptDisableRetry() throws Throwable {
HttpRequest request = mock(HttpRequest.class);
when(request.getURI()).thenReturn(new URI("http://foo"));
ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[]{}, HttpStatus.OK);
LoadBalancedRetryPolicyFactory lbRetryPolicyFactory = mock(LoadBalancedRetryPolicyFactory.class);
when(lbRetryPolicyFactory.create(eq("foo"), any(LoadBalanceChooser.class))).thenReturn(null);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("foo"))).thenReturn(serviceInstance);
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class))).thenThrow(new IOException());
lbProperties.setEnabled(false);
LoadBalancerInterceptor interceptor = new LoadBalancerInterceptor(client, retryTemplate, lbProperties, lbRetryPolicyFactory);
byte[] body = new byte[]{};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
interceptor.intercept(request, body, execution);
verify(retryTemplate, times(1)).setRetryPolicy(any(NeverRetryPolicy.class));
}
@Test
public void interceptNeverRetry() throws Throwable {
HttpRequest request = mock(HttpRequest.class);
when(request.getURI()).thenReturn(new URI("http://foo"));
ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[]{}, HttpStatus.OK);
LoadBalancedRetryPolicyFactory lbRetryPolicyFactory = mock(LoadBalancedRetryPolicyFactory.class);
when(lbRetryPolicyFactory.create(eq("foo"), any(LoadBalanceChooser.class))).thenReturn(null);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("foo"))).thenReturn(serviceInstance);
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class))).thenReturn(clientHttpResponse);
lbProperties.setEnabled(true);
LoadBalancerInterceptor interceptor = new LoadBalancerInterceptor(client, retryTemplate, lbProperties, lbRetryPolicyFactory);
byte[] body = new byte[]{};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
interceptor.intercept(request, body, execution);
verify(retryTemplate, times(1)).setRetryPolicy(any(NeverRetryPolicy.class));
}
@Test
public void interceptSuccess() throws Throwable {
HttpRequest request = mock(HttpRequest.class);
when(request.getURI()).thenReturn(new URI("http://foo"));
ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[]{}, HttpStatus.OK);
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, client,"foo");
LoadBalancedRetryPolicyFactory lbRetryPolicyFactory = mock(LoadBalancedRetryPolicyFactory.class);
when(lbRetryPolicyFactory.create(eq("foo"), any(LoadBalanceChooser.class))).thenReturn(policy);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("foo"))).thenReturn(serviceInstance);
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class))).thenReturn(clientHttpResponse);
lbProperties.setEnabled(true);
LoadBalancerInterceptor interceptor = new LoadBalancerInterceptor(client, retryTemplate, lbProperties, lbRetryPolicyFactory);
byte[] body = new byte[]{};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
assertThat(rsp, is(clientHttpResponse));
verify(retryTemplate, times(1)).setRetryPolicy(eq(interceptorRetryPolicy));
}
@Test
public void interceptRetry() throws Throwable {
HttpRequest request = mock(HttpRequest.class);
when(request.getURI()).thenReturn(new URI("http://foo"));
ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[]{}, HttpStatus.OK);
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
LoadBalancedRetryPolicyFactory lbRetryPolicyFactory = mock(LoadBalancedRetryPolicyFactory.class);
when(lbRetryPolicyFactory.create(eq("foo"), any(LoadBalanceChooser.class))).thenReturn(policy);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("foo"))).thenReturn(serviceInstance);
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class))).thenThrow(new IOException()).thenReturn(clientHttpResponse);
lbProperties.setEnabled(true);
LoadBalancerInterceptor interceptor = new LoadBalancerInterceptor(client, retryTemplate, lbProperties, lbRetryPolicyFactory);
byte[] body = new byte[]{};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
verify(client, times(2)).execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class));
assertThat(rsp, is(clientHttpResponse));
verify(retryTemplate, times(1)).setRetryPolicy(any(InterceptorRetryPolicy.class));
}
@Test(expected = IOException.class)
public void interceptFailedRetry() throws Exception {
HttpRequest request = mock(HttpRequest.class);
when(request.getURI()).thenReturn(new URI("http://foo"));
ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[]{}, HttpStatus.OK);
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
when(policy.canRetrySameServer(any(LoadBalancedRetryContext.class))).thenReturn(false);
when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(false);
LoadBalancedRetryPolicyFactory lbRetryPolicyFactory = mock(LoadBalancedRetryPolicyFactory.class);
when(lbRetryPolicyFactory.create(eq("foo"), any(LoadBalanceChooser.class))).thenReturn(policy);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("foo"))).thenReturn(serviceInstance);
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class))).thenThrow(new IOException()).thenReturn(clientHttpResponse);
lbProperties.setEnabled(true);
LoadBalancerInterceptor interceptor = new LoadBalancerInterceptor(client, retryTemplate, lbProperties, lbRetryPolicyFactory);
byte[] body = new byte[]{};
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>1.1.4.BUILD-SNAPSHOT</version>
<version>1.1.5.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-context</artifactId>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>1.1.4.BUILD-SNAPSHOT</version>
<version>1.1.5.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-cloud-starter</artifactId>
<name>spring-cloud-starter</name>