Merge pull request #137 from ryanjbaxter/ribbon-retry-logic
Retry implementation for LoadBalanced RestTemplates
This commit is contained in:
@@ -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`
|
||||
|
||||
17
pom.xml
17
pom.xml
@@ -68,6 +68,23 @@
|
||||
</additionalConfig>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>**/*.properties</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<profiles>
|
||||
|
||||
@@ -53,6 +53,16 @@
|
||||
<artifactId>spring-boot-starter-hateoas</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.jayway.jsonpath</groupId>
|
||||
<artifactId>json-path</artifactId>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ClientHttpRequestInterceptor> 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<ClientHttpRequestInterceptor> 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<ClientHttpRequestInterceptor> list = new ArrayList<>(
|
||||
restTemplate.getInterceptors());
|
||||
list.add(loadBalancerInterceptor);
|
||||
restTemplate.setInterceptors(list);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
|
||||
|
||||
/**
|
||||
* execute request using a ServiceInstance from the LoadBalancer for the specified
|
||||
* service
|
||||
* @param serviceId the service id to look up the LoadBalancer
|
||||
* @param serviceInstance the service to execute the request to
|
||||
* @param request allows implementations to execute pre and post actions such as
|
||||
* incrementing metrics
|
||||
* @return the result of the LoadBalancerRequest callback on the selected
|
||||
* ServiceInstance
|
||||
*/
|
||||
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
|
||||
|
||||
/**
|
||||
* Create a proper URI with a real host and port for systems to utilize.
|
||||
* Some systems use a URI with the logical serivce name as the host,
|
||||
@@ -54,5 +60,4 @@ public interface LoadBalancerClient {
|
||||
* @return a reconstructed URI
|
||||
*/
|
||||
URI reconstructURI(ServiceInstance instance, URI original);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ClientHttpResponse>() {
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ClientHttpResponse, IOException>() {
|
||||
@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<ClientHttpResponse>() {
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse apply(
|
||||
final ServiceInstance instance)
|
||||
throws Exception {
|
||||
HttpRequest serviceRequest = new ServiceRequestWrapper(
|
||||
request, instance, loadBalancer);
|
||||
return execution.execute(serviceRequest, body);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<URL> extractedUrls = new ArrayList<URL>();
|
||||
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<URL> extractUrlsFromManifestClassPath(URL booterJar) throws Exception {
|
||||
List<URL> urls = new ArrayList<URL>();
|
||||
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<URL> filteredUrls = new ArrayList<URL>();
|
||||
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<String> 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.<String>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<FrameworkMethod> getAnnotatedMethods(
|
||||
Class<? extends Annotation> annotationClass) {
|
||||
try {
|
||||
return getAnnotatedMethods(annotationClass.getName());
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<FrameworkMethod> getAnnotatedMethods(String annotationClassName)
|
||||
throws ClassNotFoundException {
|
||||
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) this.classLoader
|
||||
.loadClass(annotationClassName);
|
||||
List<FrameworkMethod> methods = super.getAnnotatedMethods(annotationClass);
|
||||
return wrapFrameworkMethods(methods);
|
||||
}
|
||||
|
||||
private List<FrameworkMethod> wrapFrameworkMethods(
|
||||
List<FrameworkMethod> methods) {
|
||||
List<FrameworkMethod> wrapped = new ArrayList<FrameworkMethod>(
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<String, RestTemplate> 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<String, RestTemplate> restTemplates = context
|
||||
.getBeansOfType(RestTemplate.class);
|
||||
|
||||
assertThat(restTemplates, is(notNullValue()));
|
||||
Collection<RestTemplate> 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> T execute(String serviceId, LoadBalancerRequest<T> request) {
|
||||
return request.apply(choose(serviceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
|
||||
return request.apply(choose(serviceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI reconstructURI(ServiceInstance instance, URI original) {
|
||||
return DefaultServiceInstance.getUri(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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<String, RestTemplate> 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<ClientHttpRequestInterceptor> 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<String, RestTemplate> restTemplates = context
|
||||
.getBeansOfType(RestTemplate.class);
|
||||
|
||||
assertThat(restTemplates, is(notNullValue()));
|
||||
Collection<RestTemplate> 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> T execute(String serviceId, LoadBalancerRequest<T> request) {
|
||||
return request.apply(choose(serviceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI reconstructURI(ServiceInstance instance, URI original) {
|
||||
return DefaultServiceInstance.getUri(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
|
||||
assertThat(interceptors, hasSize(1));
|
||||
ClientHttpRequestInterceptor interceptor = interceptors.get(0);
|
||||
assertThat(interceptor, is(instanceOf(RetryLoadBalancerInterceptor.class)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user