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 annotationClass) { + try { + return getAnnotatedMethods(annotationClass.getName()); + } + catch (ClassNotFoundException ex) { + throw new RuntimeException(ex); + } + } + + @SuppressWarnings("unchecked") + private List getAnnotatedMethods(String annotationClassName) + throws ClassNotFoundException { + Class annotationClass = (Class) 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