diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc
index beb51e12..1571e09c 100644
--- a/docs/src/main/asciidoc/spring-cloud-commons.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc
@@ -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`
diff --git a/pom.xml b/pom.xml
index 6f9be7e1..cbaaa00a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,6 +68,23 @@
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.0.2
+
+
+
+ test-jar
+
+
+
+
+
+ **/*.properties
+
+
+
diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml
index 26eb7881..764975ed 100644
--- a/spring-cloud-commons/pom.xml
+++ b/spring-cloud-commons/pom.xml
@@ -53,6 +53,16 @@
spring-boot-starter-hateoas
true
+
+ org.springframework.boot
+ spring-boot-starter-aop
+ true
+
+
+ org.springframework.retry
+ spring-retry
+ true
+
com.jayway.jsonpath
json-path
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicy.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicy.java
new file mode 100644
index 00000000..cd316bef
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicy.java
@@ -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 ServiceInstanceChooser serviceInstanceChooser;
+ 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 serviceInstanceChooser the load balancer client
+ * @param serviceName the name of the service
+ */
+ public InterceptorRetryPolicy(HttpRequest request, LoadBalancedRetryPolicy policy,
+ ServiceInstanceChooser serviceInstanceChooser, String serviceName) {
+ this.request = request;
+ this.policy = policy;
+ this.serviceInstanceChooser = serviceInstanceChooser;
+ 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(serviceInstanceChooser.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 (!serviceInstanceChooser.equals(that.serviceInstanceChooser)) return false;
+ return serviceName.equals(that.serviceName);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = request.hashCode();
+ result = 31 * result + policy.hashCode();
+ result = 31 * result + serviceInstanceChooser.hashCode();
+ result = 31 * result + serviceName.hashCode();
+ return result;
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryContext.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryContext.java
new file mode 100644
index 00000000..c0caebc7
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryContext.java
@@ -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;
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryPolicy.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryPolicy.java
new file mode 100644
index 00000000..02ee113c
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryPolicy.java
@@ -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);
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryPolicyFactory.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryPolicyFactory.java
new file mode 100644
index 00000000..f823792b
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryPolicyFactory.java
@@ -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 serviceInstanceChooser Used to get the next server from a load balancer
+ * @return A retry policy for the service.
+ */
+ public LoadBalancedRetryPolicy create(String serviceId, ServiceInstanceChooser serviceInstanceChooser);
+
+ static class NeverRetryFactory implements LoadBalancedRetryPolicyFactory {
+
+ @Override
+ public LoadBalancedRetryPolicy create(String serviceId, ServiceInstanceChooser serviceInstanceChooser) {
+ return null;
+ }
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java
index 88dc2f7d..a0d51d93 100644
--- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java
@@ -25,9 +25,13 @@ 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.autoconfigure.condition.ConditionalOnMissingClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+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 +43,7 @@ import org.springframework.web.client.RestTemplate;
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
+@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@@ -60,25 +65,66 @@ public class LoadBalancerAutoConfiguration {
};
}
- @Bean
- @ConditionalOnMissingBean
- public RestTemplateCustomizer restTemplateCustomizer(
- final LoadBalancerInterceptor loadBalancerInterceptor) {
- return new RestTemplateCustomizer() {
- @Override
- public void customize(RestTemplate restTemplate) {
- List list = new ArrayList<>(
- restTemplate.getInterceptors());
- list.add(loadBalancerInterceptor);
- restTemplate.setInterceptors(list);
- }
- };
+ @Configuration
+ @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
+ static class LoadBalancerInterceptorConfig {
+ @Bean
+ public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient) {
+ return new LoadBalancerInterceptor(loadBalancerClient);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public RestTemplateCustomizer restTemplateCustomizer(
+ final LoadBalancerInterceptor loadBalancerInterceptor) {
+ return new RestTemplateCustomizer() {
+ @Override
+ public void customize(RestTemplate restTemplate) {
+ List list = new ArrayList<>(
+ restTemplate.getInterceptors());
+ list.add(loadBalancerInterceptor);
+ restTemplate.setInterceptors(list);
+ }
+ };
+ }
}
- @Bean
- public LoadBalancerInterceptor ribbonInterceptor(
- LoadBalancerClient loadBalancerClient) {
- return new LoadBalancerInterceptor(loadBalancerClient);
- }
+ @Configuration
+ @ConditionalOnClass(RetryTemplate.class)
+ static class RetryAutoConfiguration {
+ @Bean
+ public RetryTemplate retryTemplate() {
+ RetryTemplate template = new RetryTemplate();
+ template.setThrowLastExceptionOnExhausted(true);
+ return template;
+ }
+ @Bean
+ @ConditionalOnMissingBean
+ public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() {
+ return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();
+ }
+
+ @Bean
+ public RetryLoadBalancerInterceptor ribbonInterceptor(
+ LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties,
+ LoadBalancedRetryPolicyFactory lbRetryPolicyFactory) {
+ return new RetryLoadBalancerInterceptor(loadBalancerClient, retryTemplate(), properties, lbRetryPolicyFactory);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public RestTemplateCustomizer restTemplateCustomizer(
+ final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
+ return new RestTemplateCustomizer() {
+ @Override
+ public void customize(RestTemplate restTemplate) {
+ List list = new ArrayList<>(
+ restTemplate.getInterceptors());
+ list.add(loadBalancerInterceptor);
+ restTemplate.setInterceptors(list);
+ }
+ };
+ }
+ }
}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClient.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClient.java
index 2a13c14c..2b060fc4 100644
--- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClient.java
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClient.java
@@ -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 ServiceInstanceChooser {
/**
* execute request using a ServiceInstance from the LoadBalancer for the specified
@@ -44,6 +38,18 @@ public interface LoadBalancerClient {
*/
T execute(String serviceId, LoadBalancerRequest 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 execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest 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);
-
}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerInterceptor.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerInterceptor.java
index 5f1877b1..0d989ae0 100644
--- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerInterceptor.java
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerInterceptor.java
@@ -18,17 +18,16 @@ 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;
/**
* @author Spencer Gibb
* @author Dave Syer
+ * @author Ryan Baxter
*/
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
@@ -45,34 +44,14 @@ public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
String serviceName = originalUri.getHost();
return this.loadBalancer.execute(serviceName,
new LoadBalancerRequest() {
-
@Override
public ClientHttpResponse apply(final ServiceInstance instance)
throws Exception {
HttpRequest serviceRequest = new ServiceRequestWrapper(request,
- instance);
+ instance, loadBalancer);
return execution.execute(serviceRequest, body);
}
});
}
-
- private class ServiceRequestWrapper extends HttpRequestWrapper {
-
- private final ServiceInstance instance;
-
- public ServiceRequestWrapper(HttpRequest request, ServiceInstance instance) {
- super(request);
- this.instance = instance;
- }
-
- @Override
- public URI getURI() {
- URI uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI(
- this.instance, getRequest().getURI());
- return uri;
- }
-
- }
-
}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRetryProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRetryProperties.java
new file mode 100644
index 00000000..634249c0
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRetryProperties.java
@@ -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;
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java
new file mode 100644
index 00000000..a5cae45f
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java
@@ -0,0 +1,77 @@
+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.retry.RetryCallback;
+import org.springframework.retry.RetryContext;
+import org.springframework.retry.policy.NeverRetryPolicy;
+import org.springframework.retry.support.RetryTemplate;
+
+/**
+ * @author Ryan Baxter
+ */
+public class RetryLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
+
+ private LoadBalancedRetryPolicyFactory lbRetryPolicyFactory;
+ private RetryTemplate retryTemplate;
+ private LoadBalancerClient loadBalancer;
+ private LoadBalancerRetryProperties lbProperties;
+
+
+ public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, RetryTemplate retryTemplate,
+ LoadBalancerRetryProperties lbProperties,
+ LoadBalancedRetryPolicyFactory lbRetryPolicyFactory) {
+ this.loadBalancer = loadBalancer;
+ this.lbRetryPolicyFactory = lbRetryPolicyFactory;
+ this.retryTemplate = retryTemplate;
+ this.lbProperties = lbProperties;
+ }
+
+ @Override
+ public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
+ final ClientHttpRequestExecution execution) throws IOException {
+ final URI originalUri = request.getURI();
+ 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() {
+ @Override
+ 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 RetryLoadBalancerInterceptor.this.loadBalancer.execute(
+ serviceName, serviceInstance,
+ new LoadBalancerRequest() {
+
+ @Override
+ public ClientHttpResponse apply(
+ final ServiceInstance instance)
+ throws Exception {
+ HttpRequest serviceRequest = new ServiceRequestWrapper(
+ request, instance, loadBalancer);
+ return execution.execute(serviceRequest, body);
+ }
+
+ });
+ }
+ });
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ServiceInstanceChooser.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ServiceInstanceChooser.java
new file mode 100644
index 00000000..4740c8fb
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ServiceInstanceChooser.java
@@ -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 ServiceInstanceChooser {
+
+ /**
+ * 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);
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ServiceRequestWrapper.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ServiceRequestWrapper.java
new file mode 100644
index 00000000..37be07f5
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ServiceRequestWrapper.java
@@ -0,0 +1,29 @@
+package org.springframework.cloud.client.loadbalancer;
+
+import java.net.URI;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.support.HttpRequestWrapper;
+
+/**
+ * @author Ryan Baxter
+ */
+public class ServiceRequestWrapper extends HttpRequestWrapper {
+ private final ServiceInstance instance;
+ private final LoadBalancerClient loadBalancer;
+
+ public ServiceRequestWrapper(HttpRequest request, ServiceInstance instance,
+ LoadBalancerClient loadBalancer) {
+ super(request);
+ this.instance = instance;
+ this.loadBalancer = loadBalancer;
+ }
+
+ @Override
+ public URI getURI() {
+ URI uri = this.loadBalancer.reconstructURI(
+ this.instance, getRequest().getURI());
+ return uri;
+ }
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/ClassPathExclusions.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/ClassPathExclusions.java
new file mode 100644
index 00000000..88f151e3
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/ClassPathExclusions.java
@@ -0,0 +1,25 @@
+package org.springframework.cloud;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Taken from Spring Boot test utils.
+ * https://github.com/spring-projects/spring-boot/blob/1.4.x/spring-boot/src/test/java/org/springframework/boot/testutil/ClassPathExclusions.java
+ * @author Ryan Baxter
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ClassPathExclusions {
+
+ /**
+ * One or more Ant-style patterns that identify entries to be excluded from the class
+ * path. Matching is performed against an entry's {@link File#getName() file name}.
+ * For example, to exclude Hibernate Validator from the classpath,
+ * {@code "hibernate-validator-*.jar"} can be used.
+ * @return the exclusion patterns
+ */
+ String[] value();
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/FilteredClassPathRunner.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/FilteredClassPathRunner.java
new file mode 100644
index 00000000..2f432a54
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/FilteredClassPathRunner.java
@@ -0,0 +1,220 @@
+package org.springframework.cloud;
+
+import java.io.File;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.TestClass;
+
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.StringUtils;
+
+/**
+ * Taken from Spring Boot test utils.
+ * https://github.com/spring-projects/spring-boot/blob/1.4.x/spring-boot/src/test/java/org/springframework/boot/testutil/FilteredClassPathRunner.java
+ * @author Ryan Baxter
+ */
+public class FilteredClassPathRunner extends BlockJUnit4ClassRunner {
+ public FilteredClassPathRunner(Class> testClass) throws InitializationError {
+ super(testClass);
+ }
+
+ @Override
+ protected TestClass createTestClass(Class> testClass) {
+ try {
+ ClassLoader classLoader = createTestClassLoader(testClass);
+ return new FilteredTestClass(classLoader, testClass.getName());
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private URLClassLoader createTestClassLoader(Class> testClass) throws Exception {
+ URLClassLoader classLoader = (URLClassLoader) this.getClass().getClassLoader();
+ return new FilteredClassLoader(filterUrls(extractUrls(classLoader), testClass),
+ classLoader.getParent(), classLoader);
+ }
+
+ private URL[] extractUrls(URLClassLoader classLoader) throws Exception {
+ List extractedUrls = new ArrayList();
+ for (URL url : classLoader.getURLs()) {
+ if (isSurefireBooterJar(url)) {
+ extractedUrls.addAll(extractUrlsFromManifestClassPath(url));
+ }
+ else {
+ extractedUrls.add(url);
+ }
+ }
+ return extractedUrls.toArray(new URL[extractedUrls.size()]);
+ }
+
+ private boolean isSurefireBooterJar(URL url) {
+ return url.getPath().contains("surefirebooter");
+ }
+
+ private List extractUrlsFromManifestClassPath(URL booterJar) throws Exception {
+ List urls = new ArrayList();
+ for (String entry : getClassPath(booterJar)) {
+ urls.add(new URL(entry));
+ }
+ return urls;
+ }
+
+ private String[] getClassPath(URL booterJar) throws Exception {
+ JarFile jarFile = new JarFile(new File(booterJar.toURI()));
+ try {
+ return StringUtils.delimitedListToStringArray(jarFile.getManifest()
+ .getMainAttributes().getValue(Attributes.Name.CLASS_PATH), " ");
+ }
+ finally {
+ jarFile.close();
+ }
+ }
+
+ private URL[] filterUrls(URL[] urls, Class> testClass) throws Exception {
+ ClassPathEntryFilter filter = new ClassPathEntryFilter(testClass);
+ List filteredUrls = new ArrayList();
+ for (URL url : urls) {
+ if (!filter.isExcluded(url)) {
+ filteredUrls.add(url);
+ }
+ }
+ return filteredUrls.toArray(new URL[filteredUrls.size()]);
+ }
+
+ /**
+ * Filter for class path entries.
+ */
+ private static final class ClassPathEntryFilter {
+
+ private final List exclusions;
+
+ private final AntPathMatcher matcher = new AntPathMatcher();
+
+ private ClassPathEntryFilter(Class> testClass) throws Exception {
+ ClassPathExclusions exclusions = AnnotationUtils.findAnnotation(testClass,
+ ClassPathExclusions.class);
+ this.exclusions = exclusions == null ? Collections.emptyList()
+ : Arrays.asList(exclusions.value());
+ }
+
+ private boolean isExcluded(URL url) throws Exception {
+ if (!"file".equals(url.getProtocol())) {
+ return false;
+ }
+ String name = new File(url.toURI()).getName();
+ for (String exclusion : this.exclusions) {
+ if (this.matcher.match(exclusion, name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Filtered version of JUnit's {@link TestClass}.
+ */
+ private static final class FilteredTestClass extends TestClass {
+
+ private final ClassLoader classLoader;
+
+ FilteredTestClass(ClassLoader classLoader, String testClassName)
+ throws ClassNotFoundException {
+ super(classLoader.loadClass(testClassName));
+ this.classLoader = classLoader;
+ }
+
+ @Override
+ public List getAnnotatedMethods(
+ Class extends Annotation> annotationClass) {
+ try {
+ return getAnnotatedMethods(annotationClass.getName());
+ }
+ catch (ClassNotFoundException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private List getAnnotatedMethods(String annotationClassName)
+ throws ClassNotFoundException {
+ Class extends Annotation> annotationClass = (Class extends Annotation>) this.classLoader
+ .loadClass(annotationClassName);
+ List methods = super.getAnnotatedMethods(annotationClass);
+ return wrapFrameworkMethods(methods);
+ }
+
+ private List wrapFrameworkMethods(
+ List methods) {
+ List wrapped = new ArrayList(
+ methods.size());
+ for (FrameworkMethod frameworkMethod : methods) {
+ wrapped.add(new FilteredFrameworkMethod(this.classLoader,
+ frameworkMethod.getMethod()));
+ }
+ return wrapped;
+ }
+
+ }
+
+ /**
+ * Filtered version of JUnit's {@link FrameworkMethod}.
+ */
+ private static final class FilteredFrameworkMethod extends FrameworkMethod {
+
+ private final ClassLoader classLoader;
+
+ private FilteredFrameworkMethod(ClassLoader classLoader, Method method) {
+ super(method);
+ this.classLoader = classLoader;
+ }
+
+ @Override
+ public Object invokeExplosively(Object target, Object... params)
+ throws Throwable {
+ ClassLoader originalClassLoader = Thread.currentThread()
+ .getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(this.classLoader);
+ try {
+ return super.invokeExplosively(target, params);
+ }
+ finally {
+ Thread.currentThread().setContextClassLoader(originalClassLoader);
+ }
+ }
+
+ }
+
+ private static final class FilteredClassLoader extends URLClassLoader {
+
+ private final ClassLoader junitLoader;
+
+ FilteredClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) {
+ super(urls, parent);
+ this.junitLoader = junitLoader;
+ }
+
+ @Override
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ if (name.startsWith("org.junit")) {
+ return this.junitLoader.loadClass(name);
+ }
+ return super.loadClass(name);
+ }
+
+ }
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java
new file mode 100644
index 00000000..52f5a290
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java
@@ -0,0 +1,157 @@
+package org.springframework.cloud.client.loadbalancer;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import lombok.SneakyThrows;
+
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.context.ConfigurableApplicationContext;
+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.web.client.RestTemplate;
+
+/**
+ * @author Ryan Baxter
+ */
+public abstract class AbstractLoadBalancerAutoConfigurationTests {
+
+ @Test
+ public void restTemplateGetsLoadBalancerInterceptor() {
+ ConfigurableApplicationContext context = init(OneRestTemplate.class);
+ final Map restTemplates = context
+ .getBeansOfType(RestTemplate.class);
+
+ assertThat(restTemplates, is(notNullValue()));
+ assertThat(restTemplates.values(), hasSize(1));
+ RestTemplate restTemplate = restTemplates.values().iterator().next();
+ assertThat(restTemplate, is(notNullValue()));
+
+ assertLoadBalanced(restTemplate);
+ }
+
+ protected abstract void assertLoadBalanced(RestTemplate restTemplate);
+
+ @Test
+ public void multipleRestTemplates() {
+ ConfigurableApplicationContext context = init(TwoRestTemplates.class);
+ final Map restTemplates = context
+ .getBeansOfType(RestTemplate.class);
+
+ assertThat(restTemplates, is(notNullValue()));
+ Collection templates = restTemplates.values();
+ assertThat(templates, hasSize(2));
+
+ TwoRestTemplates.Two two = context.getBean(TwoRestTemplates.Two.class);
+
+ assertThat(two.loadBalanced, is(notNullValue()));
+ assertLoadBalanced(two.loadBalanced);
+
+ assertThat(two.nonLoadBalanced, is(notNullValue()));
+ assertThat(two.nonLoadBalanced.getInterceptors(), is(empty()));
+ }
+
+ protected ConfigurableApplicationContext init(Class> config) {
+ return new SpringApplicationBuilder().web(false)
+ .properties("spring.aop.proxyTargetClass=true")
+ .sources(config, LoadBalancerAutoConfiguration.class).run();
+ }
+
+ @Configuration
+ protected static class OneRestTemplate {
+
+ @LoadBalanced
+ @Bean
+ RestTemplate loadBalancedRestTemplate() {
+ return new RestTemplate();
+ }
+
+ @Bean
+ LoadBalancerClient loadBalancerClient() {
+ return new NoopLoadBalancerClient();
+ }
+
+ @Bean
+ LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() { return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();}
+
+ }
+
+ @Configuration
+ protected static class TwoRestTemplates {
+
+ @Primary
+ @Bean
+ RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+
+ @LoadBalanced
+ @Bean
+ RestTemplate loadBalancedRestTemplate() {
+ return new RestTemplate();
+ }
+
+ @Bean
+ LoadBalancerClient loadBalancerClient() {
+ return new NoopLoadBalancerClient();
+ }
+
+ @Bean
+ LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() { return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();}
+
+ @Configuration
+ protected static class Two {
+ @Autowired
+ RestTemplate nonLoadBalanced;
+
+ @Autowired
+ @LoadBalanced
+ RestTemplate loadBalanced;
+ }
+
+ }
+
+ private static class NoopLoadBalancerClient implements LoadBalancerClient {
+ private final Random random = new Random();
+
+ @Override
+ public ServiceInstance choose(String serviceId) {
+ return new DefaultServiceInstance(serviceId, serviceId,
+ this.random.nextInt(40000), false);
+ }
+
+ @Override
+ @SneakyThrows
+ public T execute(String serviceId, LoadBalancerRequest request) {
+ return request.apply(choose(serviceId));
+ }
+
+ @Override
+ @SneakyThrows
+ public T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException {
+ return request.apply(choose(serviceId));
+ }
+
+ @Override
+ public URI reconstructURI(ServiceInstance instance, URI original) {
+ return DefaultServiceInstance.getUri(instance);
+ }
+ }
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicyTest.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicyTest.java
new file mode 100644
index 00000000..8db72f71
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicyTest.java
@@ -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 ServiceInstanceChooser serviceInstanceChooser;
+ private String serviceName;
+
+ @Before
+ public void setup() {
+ request = mock(HttpRequest.class);
+ policy = mock(LoadBalancedRetryPolicy.class);
+ serviceInstanceChooser = mock(ServiceInstanceChooser.class);
+ serviceName = "foo";
+ }
+
+ @After
+ public void teardown() {
+ request = null;
+ policy = null;
+ serviceInstanceChooser = null;
+ serviceName = null;
+ }
+
+ @Test
+ public void canRetryBeforeExecution() throws Exception {
+ InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, serviceInstanceChooser, serviceName);
+ LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
+ when(context.getRetryCount()).thenReturn(0);
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(serviceInstanceChooser.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, serviceInstanceChooser, 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, serviceInstanceChooser, 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, serviceInstanceChooser, serviceName);
+ RetryContext context = interceptorRetryPolicy.open(null);
+ assertThat(context, IsInstanceOf.instanceOf(LoadBalancedRetryContext.class));
+ }
+
+ @Test
+ public void close() throws Exception {
+ InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy, serviceInstanceChooser, 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, serviceInstanceChooser, 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(serviceInstanceChooser.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, serviceInstanceChooser, 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, serviceInstanceChooser, serviceName)), is(true));
+ }
+
+}
\ No newline at end of file
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryContextTest.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryContextTest.java
new file mode 100644
index 00000000..6e7f9aa6
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryContextTest.java
@@ -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));
+ }
+}
\ No newline at end of file
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java
index 3606fa0d..73b8cdba 100644
--- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java
@@ -1,149 +1,29 @@
package org.springframework.cloud.client.loadbalancer;
-import java.net.URI;
-import java.util.Collection;
import java.util.List;
-import java.util.Map;
-import java.util.Random;
-
-import org.junit.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.builder.SpringApplicationBuilder;
-import org.springframework.cloud.client.DefaultServiceInstance;
-import org.springframework.cloud.client.ServiceInstance;
-import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
+import org.junit.runner.RunWith;
+import org.springframework.cloud.ClassPathExclusions;
+import org.springframework.cloud.FilteredClassPathRunner;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
-import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.junit.Assert.assertThat;
-
-import lombok.SneakyThrows;
/**
* @author Spencer Gibb
*/
-public class LoadBalancerAutoConfigurationTests {
-
- @Test
- public void restTemplateGetsLoadBalancerInterceptor() {
- ConfigurableApplicationContext context = init(OneRestTemplate.class);
- final Map restTemplates = context
- .getBeansOfType(RestTemplate.class);
-
- assertThat(restTemplates, is(notNullValue()));
- assertThat(restTemplates.values(), hasSize(1));
- RestTemplate restTemplate = restTemplates.values().iterator().next();
- assertThat(restTemplate, is(notNullValue()));
-
- assertLoadBalanced(restTemplate);
- }
+@RunWith(FilteredClassPathRunner.class)
+@ClassPathExclusions({"spring-retry-*.jar", "spring-boot-starter-aop-*.jar"})
+public class LoadBalancerAutoConfigurationTests extends AbstractLoadBalancerAutoConfigurationTests {
+ @Override
protected void assertLoadBalanced(RestTemplate restTemplate) {
List interceptors = restTemplate.getInterceptors();
assertThat(interceptors, hasSize(1));
ClientHttpRequestInterceptor interceptor = interceptors.get(0);
assertThat(interceptor, is(instanceOf(LoadBalancerInterceptor.class)));
}
-
- @Test
- public void multipleRestTemplates() {
- ConfigurableApplicationContext context = init(TwoRestTemplates.class);
- final Map restTemplates = context
- .getBeansOfType(RestTemplate.class);
-
- assertThat(restTemplates, is(notNullValue()));
- Collection templates = restTemplates.values();
- assertThat(templates, hasSize(2));
-
- TwoRestTemplates.Two two = context.getBean(TwoRestTemplates.Two.class);
-
- assertThat(two.loadBalanced, is(notNullValue()));
- assertLoadBalanced(two.loadBalanced);
-
- assertThat(two.nonLoadBalanced, is(notNullValue()));
- assertThat(two.nonLoadBalanced.getInterceptors(), is(empty()));
- }
-
- protected ConfigurableApplicationContext init(Class> config) {
- return new SpringApplicationBuilder().web(false)
- .properties("spring.aop.proxyTargetClass=true")
- .sources(config, LoadBalancerAutoConfiguration.class).run();
- }
-
- @Configuration
- protected static class OneRestTemplate {
-
- @LoadBalanced
- @Bean
- RestTemplate loadBalancedRestTemplate() {
- return new RestTemplate();
- }
-
- @Bean
- LoadBalancerClient loadBalancerClient() {
- return new NoopLoadBalancerClient();
- }
-
- }
-
- @Configuration
- protected static class TwoRestTemplates {
-
- @Primary
- @Bean
- RestTemplate restTemplate() {
- return new RestTemplate();
- }
-
- @LoadBalanced
- @Bean
- RestTemplate loadBalancedRestTemplate() {
- return new RestTemplate();
- }
-
- @Bean
- LoadBalancerClient loadBalancerClient() {
- return new NoopLoadBalancerClient();
- }
-
- @Configuration
- protected static class Two {
- @Autowired
- RestTemplate nonLoadBalanced;
-
- @Autowired
- @LoadBalanced
- RestTemplate loadBalanced;
- }
-
- }
-
- private static class NoopLoadBalancerClient implements LoadBalancerClient {
- private final Random random = new Random();
-
- @Override
- public ServiceInstance choose(String serviceId) {
- return new DefaultServiceInstance(serviceId, serviceId,
- this.random.nextInt(40000), false);
- }
-
- @Override
- @SneakyThrows
- public T execute(String serviceId, LoadBalancerRequest request) {
- return request.apply(choose(serviceId));
- }
-
- @Override
- public URI reconstructURI(ServiceInstance instance, URI original) {
- return DefaultServiceInstance.getUri(instance);
- }
- }
}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerAutoConfigurationTests.java
new file mode 100644
index 00000000..9d51bbb2
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerAutoConfigurationTests.java
@@ -0,0 +1,25 @@
+package org.springframework.cloud.client.loadbalancer;
+
+import java.util.List;
+
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.web.client.RestTemplate;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * @author Ryan Baxter
+ */
+public class RetryLoadBalancerAutoConfigurationTests extends AbstractLoadBalancerAutoConfigurationTests {
+ @Override
+ protected void assertLoadBalanced(RestTemplate restTemplate) {
+ List interceptors = restTemplate.getInterceptors();
+ assertThat(interceptors, hasSize(1));
+ ClientHttpRequestInterceptor interceptor = interceptors.get(0);
+ assertThat(interceptor, is(instanceOf(RetryLoadBalancerInterceptor.class)));
+ }
+}
+
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptorTest.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptorTest.java
new file mode 100644
index 00000000..1b4ba551
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptorTest.java
@@ -0,0 +1,152 @@
+package org.springframework.cloud.client.loadbalancer;
+
+import java.io.IOException;
+import java.net.URI;
+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 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 RetryLoadBalancerInterceptorTest {
+
+ 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(ServiceInstanceChooser.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);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(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(ServiceInstanceChooser.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);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(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(ServiceInstanceChooser.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);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(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(ServiceInstanceChooser.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);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(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(ServiceInstanceChooser.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);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, retryTemplate, lbProperties, lbRetryPolicyFactory);
+ byte[] body = new byte[]{};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
+ }
+}
\ No newline at end of file