diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index e26b1ae3..64c9c7fc 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -2461,6 +2461,22 @@ certain Ribbon properties. The properties you can use are `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. +In addition you may want to retry requests when certain status codes are returned in the +response. You can list the response codes you would like the Ribbon client to retry using the + property `clientName.ribbon.retryableStatusCodes`. For example + +[source,yaml] +---- +clientName: + ribbon: + retryableStatusCodes: 404,502 +---- + +You can also create a bean of type `LoadBalancedRetryPolicy` and implement the `retryableStatusCode` +method to determine whether you want to retry a request given the status code. + + + ==== Zuul You can turn off Zuul's retry functionality by setting `zuul.retryable` to `false`. You diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancer.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancer.java index 97071e6a..ec1f665e 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancer.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancer.java @@ -29,6 +29,7 @@ import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFact import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; +import org.springframework.cloud.netflix.ribbon.support.RetryableStatusCodeException; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.policy.NeverRetryPolicy; @@ -69,7 +70,7 @@ public class RetryableFeignLoadBalancer extends FeignLoadBalancer implements Ser else { options = new Request.Options(this.connectTimeout, this.readTimeout); } - LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create(this.getClientName(), this); + final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create(this.getClientName(), this); RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.setRetryPolicy(retryPolicy == null ? new NeverRetryPolicy() : new FeignRetryPolicy(request.toHttpRequest(), retryPolicy, this, this.getClientName())); @@ -89,6 +90,9 @@ public class RetryableFeignLoadBalancer extends FeignLoadBalancer implements Ser feignRequest = request.toRequest(); } Response response = request.client().execute(feignRequest, options); + if(retryPolicy.retryableStatusCode(response.status())) { + throw new RetryableStatusCodeException(RetryableFeignLoadBalancer.this.getClientName(), response.status()); + } return new RibbonResponse(request.getUri(), response); } }); diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java index 3eb7737e..26b99997 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java @@ -90,12 +90,14 @@ public class RibbonAutoConfiguration { @Bean @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate") + @ConditionalOnMissingBean public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory(SpringClientFactory clientFactory) { return new RibbonLoadBalancedRetryPolicyFactory(clientFactory); } @Bean @ConditionalOnMissingClass(value = "org.springframework.retry.support.RetryTemplate") + @ConditionalOnMissingBean public LoadBalancedRetryPolicyFactory neverRetryPolicyFactory() { return new LoadBalancedRetryPolicyFactory.NeverRetryFactory(); } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicy.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicy.java new file mode 100644 index 00000000..4e062ae5 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicy.java @@ -0,0 +1,123 @@ +/* + * + * * 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.netflix.ribbon; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; +import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpMethod; +import org.springframework.util.StringUtils; +import com.netflix.client.config.CommonClientConfigKey; +import com.netflix.client.config.IClientConfig; +import com.netflix.client.config.IClientConfigKey; + +/** + * {@link LoadBalancedRetryPolicy} for Ribbon clients. + * @author Ryan Baxter + */ +public class RibbonLoadBalancedRetryPolicy implements LoadBalancedRetryPolicy { + + public static final IClientConfigKey RETRYABLE_STATUS_CODES = new CommonClientConfigKey("retryableStatusCodes") {}; + private int sameServerCount = 0; + private int nextServerCount = 0; + private String serviceId; + private RibbonLoadBalancerContext lbContext; + private ServiceInstanceChooser loadBalanceChooser; + List retryableStatusCodes = new ArrayList<>(); + + public RibbonLoadBalancedRetryPolicy(String serviceId, RibbonLoadBalancerContext context, ServiceInstanceChooser loadBalanceChooser) { + this.serviceId = serviceId; + this.lbContext = context; + this.loadBalanceChooser = loadBalanceChooser; + } + + public RibbonLoadBalancedRetryPolicy(String serviceId, RibbonLoadBalancerContext context, ServiceInstanceChooser loadBalanceChooser, + IClientConfig clientConfig) { + this.serviceId = serviceId; + this.lbContext = context; + this.loadBalanceChooser = loadBalanceChooser; + String retryableStatusCodesProp = clientConfig.getPropertyAsString(RETRYABLE_STATUS_CODES, ""); + String[] retryableStatusCodesArray = retryableStatusCodesProp.split(","); + for(String code : retryableStatusCodesArray) { + if(!StringUtils.isEmpty(code)) { + try { + retryableStatusCodes.add(Integer.valueOf(code)); + } catch (NumberFormatException e) { + //TODO log + } + } + } + } + + public boolean canRetry(LoadBalancedRetryContext context) { + HttpMethod method = context.getRequest().getMethod(); + return HttpMethod.GET == method || lbContext.isOkToRetryOnAllOperations(); + } + + @Override + public boolean canRetrySameServer(LoadBalancedRetryContext context) { + return sameServerCount < lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context); + } + + @Override + public boolean canRetryNextServer(LoadBalancedRetryContext context) { + //this will be called after a failure occurs and we increment the counter + //so we check that the count is less than or equals to too make sure + //we try the next server the right number of times + return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer() && canRetry(context); + } + + @Override + public void close(LoadBalancedRetryContext context) { + + } + + @Override + public void registerThrowable(LoadBalancedRetryContext context, Throwable throwable) { + //Check if we need to ask the load balancer for a new server. + //Do this before we increment the counters because the first call to this method + //is not a retry it is just an initial failure. + if(!canRetrySameServer(context) && canRetryNextServer(context)) { + context.setServiceInstance(loadBalanceChooser.choose(serviceId)); + } + //This method is called regardless of whether we are retrying or making the first request. + //Since we do not count the initial request in the retry count we don't reset the counter + //until we actually equal the same server count limit. This will allow us to make the initial + //request plus the right number of retries. + if(sameServerCount >= lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context)) { + //reset same server since we are moving to a new server + sameServerCount = 0; + nextServerCount++; + if(!canRetryNextServer(context)) { + context.setExhaustedOnly(); + } + } else { + sameServerCount++; + } + + } + + @Override + public boolean retryableStatusCode(int statusCode) { + return retryableStatusCodes.contains(statusCode); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicyFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicyFactory.java index 563071c3..e20a8c78 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicyFactory.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicyFactory.java @@ -15,12 +15,9 @@ */ package org.springframework.cloud.netflix.ribbon; -import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; -import org.springframework.http.HttpMethod; /** * @author Ryan Baxter @@ -34,60 +31,10 @@ public class RibbonLoadBalancedRetryPolicyFactory implements LoadBalancedRetryPo } @Override - public LoadBalancedRetryPolicy create(final String serviceId, final ServiceInstanceChooser loadBalanceChooser) { - final RibbonLoadBalancerContext lbContext = this.clientFactory + public LoadBalancedRetryPolicy create(String serviceId, ServiceInstanceChooser loadBalanceChooser) { + RibbonLoadBalancerContext lbContext = this.clientFactory .getLoadBalancerContext(serviceId); - return new LoadBalancedRetryPolicy() { - private int sameServerCount = 0; - private int nextServerCount = 0; - private ServiceInstance lastServiceInstance = null; - public boolean canRetry(LoadBalancedRetryContext context) { - HttpMethod method = context.getRequest().getMethod(); - return HttpMethod.GET == method || lbContext.isOkToRetryOnAllOperations(); - } - - @Override - public boolean canRetrySameServer(LoadBalancedRetryContext context) { - return sameServerCount < lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context); - } - - @Override - public boolean canRetryNextServer(LoadBalancedRetryContext context) { - //this will be called after a failure occurs and we increment the counter - //so we check that the count is less than or equals to too make sure - //we try the next server the right number of times - return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer() && canRetry(context); - } - - @Override - public void close(LoadBalancedRetryContext context) { - - } - - @Override - public void registerThrowable(LoadBalancedRetryContext context, Throwable throwable) { - //Check if we need to ask the load balancer for a new server. - //Do this before we increment the counters because the first call to this method - //is not a retry it is just an initial failure. - if(!canRetrySameServer(context) && canRetryNextServer(context)) { - context.setServiceInstance(loadBalanceChooser.choose(serviceId)); - } - //This method is called regardless of whether we are retrying or making the first request. - //Since we do not count the initial request in the retry count we don't reset the counter - //until we actually equal the same server count limit. This will allow us to make the initial - //request plus the right number of retries. - if(sameServerCount >= lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context)) { - //reset same server since we are moving to a new server - sameServerCount = 0; - nextServerCount++; - if(!canRetryNextServer(context)) { - context.setExhaustedOnly(); - } - } else { - sameServerCount++; - } - - } - }; + return new RibbonLoadBalancedRetryPolicy(serviceId, lbContext, loadBalanceChooser, clientFactory.getClientConfig(serviceId)); } } + diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RetryableRibbonLoadBalancingHttpClient.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RetryableRibbonLoadBalancingHttpClient.java index 92b080b4..fe3629a1 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RetryableRibbonLoadBalancingHttpClient.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RetryableRibbonLoadBalancingHttpClient.java @@ -29,6 +29,7 @@ import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; import org.springframework.cloud.netflix.feign.ribbon.FeignRetryPolicy; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; +import org.springframework.cloud.netflix.ribbon.support.RetryableStatusCodeException; import org.springframework.http.HttpRequest; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; @@ -65,7 +66,8 @@ public class RetryableRibbonLoadBalancingHttpClient extends RibbonLoadBalancingH CommonClientConfigKey.FollowRedirects, this.followRedirects)); final RequestConfig requestConfig = builder.build(); - return this.executeWithRetry(request, new RetryCallback() { + final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create(this.getClientName(), this); + RetryCallback retryCallback = new RetryCallback() { @Override public RibbonApacheHttpResponse doWithRetry(RetryContext context) throws Exception { //on retries the policy will choose the server and set it in the context @@ -88,13 +90,17 @@ public class RetryableRibbonLoadBalancingHttpClient extends RibbonLoadBalancingH } HttpUriRequest httpUriRequest = newRequest.toRequest(requestConfig); final HttpResponse httpResponse = RetryableRibbonLoadBalancingHttpClient.this.delegate.execute(httpUriRequest); + if(retryPolicy.retryableStatusCode(httpResponse.getStatusLine().getStatusCode())) { + throw new RetryableStatusCodeException(RetryableRibbonLoadBalancingHttpClient.this.clientName, + httpResponse.getStatusLine().getStatusCode()); + } return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI()); } - }); + }; + return this.executeWithRetry(request, retryPolicy, retryCallback); } - private RibbonApacheHttpResponse executeWithRetry(RibbonApacheHttpRequest request, RetryCallback callback) throws Exception { - LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create(this.getClientName(), this); + private RibbonApacheHttpResponse executeWithRetry(RibbonApacheHttpRequest request, LoadBalancedRetryPolicy retryPolicy, RetryCallback callback) throws Exception { RetryTemplate retryTemplate = new RetryTemplate(); boolean retryable = request.getContext() == null ? true : BooleanUtils.toBooleanDefaultIfNull(request.getContext().getRetryable(), true); diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/RetryableOkHttpLoadBalancingClient.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/RetryableOkHttpLoadBalancingClient.java index e5ae121d..eb7326c2 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/RetryableOkHttpLoadBalancingClient.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/RetryableOkHttpLoadBalancingClient.java @@ -19,7 +19,6 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import java.io.IOException; import java.net.URI; import org.apache.commons.lang.BooleanUtils; import org.springframework.cloud.client.ServiceInstance; @@ -30,6 +29,7 @@ import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; import org.springframework.cloud.netflix.feign.ribbon.FeignRetryPolicy; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; +import org.springframework.cloud.netflix.ribbon.support.RetryableStatusCodeException; import org.springframework.http.HttpRequest; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; @@ -55,10 +55,9 @@ public class RetryableOkHttpLoadBalancingClient extends OkHttpLoadBalancingClien this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; } - private OkHttpRibbonResponse executeWithRetry(OkHttpRibbonRequest request, - RetryCallback callback) + private OkHttpRibbonResponse executeWithRetry(OkHttpRibbonRequest request, LoadBalancedRetryPolicy retryPolicy, + RetryCallback callback) throws Exception { - LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create(this.getClientName(), this); RetryTemplate retryTemplate = new RetryTemplate(); boolean retryable = request.getContext() == null ? true : BooleanUtils.toBooleanDefaultIfNull(request.getContext().getRetryable(), true); @@ -70,7 +69,8 @@ public class RetryableOkHttpLoadBalancingClient extends OkHttpLoadBalancingClien @Override public OkHttpRibbonResponse execute(final OkHttpRibbonRequest ribbonRequest, final IClientConfig configOverride) throws Exception { - return this.executeWithRetry(ribbonRequest, new RetryCallback() { + final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create(this.getClientName(), this); + RetryCallback retryCallback = new RetryCallback() { @Override public OkHttpRibbonResponse doWithRetry(RetryContext context) throws Exception { //on retries the policy will choose the server and set it in the context @@ -95,9 +95,13 @@ public class RetryableOkHttpLoadBalancingClient extends OkHttpLoadBalancingClien final Request request = newRequest.toRequest(); Response response = httpClient.newCall(request).execute(); + if(retryPolicy.retryableStatusCode(response.code())) { + throw new RetryableStatusCodeException(RetryableOkHttpLoadBalancingClient.this.clientName, response.code()); + } return new OkHttpRibbonResponse(response, newRequest.getUri()); } - }); + }; + return this.executeWithRetry(ribbonRequest, retryPolicy, retryCallback); } @Override diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeException.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeException.java new file mode 100644 index 00000000..03268158 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeException.java @@ -0,0 +1,34 @@ +/* + * + * * 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.netflix.ribbon.support; + +import java.io.IOException; + +/** + * Exception to be thrown when the status code is deemed to be retryable. + * @author Ryan Baxter + */ +public class RetryableStatusCodeException extends IOException { + + private static final String MESSAGE = "Service %s returned a status code of %d"; + + public RetryableStatusCodeException(String serviceId, int statusCode) { + super(String.format(MESSAGE, serviceId, statusCode)); + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancerTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancerTest.java index eda21e44..e571126f 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancerTest.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancerTest.java @@ -37,6 +37,7 @@ import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; import org.springframework.cloud.netflix.ribbon.DefaultServerIntrospector; +import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryPolicy; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryPolicyFactory; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerContext; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; @@ -44,6 +45,7 @@ import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.http.HttpRequest; import com.netflix.client.RequestSpecificRetryHandler; +import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.Server; @@ -60,6 +62,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; @@ -98,6 +101,14 @@ public class RetryableFeignLoadBalancerTest { RibbonLoadBalancerContext lbContext = new RibbonLoadBalancerContext(lb, config); SpringClientFactory clientFactory = mock(SpringClientFactory.class); doReturn(lbContext).when(clientFactory).getLoadBalancerContext(any(String.class)); + IClientConfig config = mock(IClientConfig.class); + doReturn(1).when(config).get(eq(CommonClientConfigKey.MaxAutoRetries), anyInt()); + doReturn(1).when(config).get(eq(CommonClientConfigKey.MaxAutoRetriesNextServer), anyInt()); + doReturn(true).when(config).get(eq(CommonClientConfigKey.OkToRetryOnAllOperations), eq(false)); + doReturn(defaultConnectTimeout).when(config).get(eq(CommonClientConfigKey.ConnectTimeout)); + doReturn(defaultReadTimeout).when(config).get(eq(CommonClientConfigKey.ReadTimeout)); + doReturn("404,502,foo, ,").when(config).getPropertyAsString(eq(RibbonLoadBalancedRetryPolicy.RETRYABLE_STATUS_CODES),eq("")); + doReturn(config).when(clientFactory).getClientConfig(eq("default")); RibbonLoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory = new RibbonLoadBalancedRetryPolicyFactory(clientFactory); HttpRequest springRequest = mock(HttpRequest.class); Request feignRequest = Request.create("GET", "http://foo", new HashMap>(), @@ -139,6 +150,14 @@ public class RetryableFeignLoadBalancerTest { public void executeRetry() throws Exception { RibbonLoadBalancerContext lbContext = new RibbonLoadBalancerContext(lb, config); SpringClientFactory clientFactory = mock(SpringClientFactory.class); + IClientConfig config = mock(IClientConfig.class); + doReturn(1).when(config).get(eq(CommonClientConfigKey.MaxAutoRetries), anyInt()); + doReturn(1).when(config).get(eq(CommonClientConfigKey.MaxAutoRetriesNextServer), anyInt()); + doReturn(true).when(config).get(eq(CommonClientConfigKey.OkToRetryOnAllOperations), eq(false)); + doReturn(defaultConnectTimeout).when(config).get(eq(CommonClientConfigKey.ConnectTimeout)); + doReturn(defaultReadTimeout).when(config).get(eq(CommonClientConfigKey.ReadTimeout)); + doReturn("").when(config).getPropertyAsString(eq(RibbonLoadBalancedRetryPolicy.RETRYABLE_STATUS_CODES),eq("")); + doReturn(config).when(clientFactory).getClientConfig(eq("default")); doReturn(lbContext).when(clientFactory).getLoadBalancerContext(any(String.class)); RibbonLoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory = new RibbonLoadBalancedRetryPolicyFactory(clientFactory); HttpRequest springRequest = mock(HttpRequest.class); @@ -154,6 +173,34 @@ public class RetryableFeignLoadBalancerTest { verify(client, times(2)).execute(any(Request.class), any(Request.Options.class)); } + @Test + public void executeRetryOnStatusCode() throws Exception { + RibbonLoadBalancerContext lbContext = new RibbonLoadBalancerContext(lb, config); + SpringClientFactory clientFactory = mock(SpringClientFactory.class); + IClientConfig config = mock(IClientConfig.class); + doReturn(1).when(config).get(eq(CommonClientConfigKey.MaxAutoRetries), anyInt()); + doReturn(1).when(config).get(eq(CommonClientConfigKey.MaxAutoRetriesNextServer), anyInt()); + doReturn(true).when(config).get(eq(CommonClientConfigKey.OkToRetryOnAllOperations), eq(false)); + doReturn(defaultConnectTimeout).when(config).get(eq(CommonClientConfigKey.ConnectTimeout)); + doReturn(defaultReadTimeout).when(config).get(eq(CommonClientConfigKey.ReadTimeout)); + doReturn("404").when(config).getPropertyAsString(eq(RibbonLoadBalancedRetryPolicy.RETRYABLE_STATUS_CODES),eq("")); + doReturn(config).when(clientFactory).getClientConfig(eq("default")); + doReturn(lbContext).when(clientFactory).getLoadBalancerContext(any(String.class)); + RibbonLoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory = new RibbonLoadBalancedRetryPolicyFactory(clientFactory); + HttpRequest springRequest = mock(HttpRequest.class); + Request feignRequest = Request.create("GET", "http://foo", new HashMap>(), + new byte[]{}, StandardCharsets.UTF_8); + Client client = mock(Client.class); + FeignLoadBalancer.RibbonRequest request = new FeignLoadBalancer.RibbonRequest(client, feignRequest, new URI("http://foo")); + Response response = Response.builder().status(200).headers(new HashMap>()).build(); + Response fourOFourResponse = Response.builder().status(404).headers(new HashMap>()).build(); + doReturn(fourOFourResponse).doReturn(response).when(client).execute(any(Request.class), any(Request.Options.class)); + RetryableFeignLoadBalancer feignLb = new RetryableFeignLoadBalancer(lb, config, inspector, loadBalancedRetryPolicyFactory); + FeignLoadBalancer.RibbonResponse ribbonResponse = feignLb.execute(request, null); + assertEquals(200, ribbonResponse.toResponse().status()); + verify(client, times(2)).execute(any(Request.class), any(Request.Options.class)); + } + @Test public void getRequestSpecificRetryHandler() throws Exception { RibbonLoadBalancerContext lbContext = new RibbonLoadBalancerContext(lb, config); diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicyFactoryTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicyFactoryTest.java index aecf19bc..3fedccea 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicyFactoryTest.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonLoadBalancedRetryPolicyFactoryTest.java @@ -86,6 +86,7 @@ public class RibbonLoadBalancedRetryPolicyFactoryTest { doReturn(nextServer).when(config).getPropertyAsInteger(eq(CommonClientConfigKey.MaxAutoRetriesNextServer), anyInt()); doReturn(retryOnAllOps).when(config).get(eq(CommonClientConfigKey.OkToRetryOnAllOperations), anyBoolean()); doReturn(retryOnAllOps).when(config).getPropertyAsBoolean(eq(CommonClientConfigKey.OkToRetryOnAllOperations), anyBoolean()); + doReturn("").when(config).getPropertyAsString(eq(RibbonLoadBalancedRetryPolicy.RETRYABLE_STATUS_CODES),eq("")); doReturn(server.getServiceId()).when(config).getClientName(); doReturn(config).when(clientFactory).getClientConfig(eq(server.getServiceId())); clientFactory.getLoadBalancerContext(server.getServiceId()).setRetryHandler(new DefaultLoadBalancerRetryHandler(config)); @@ -97,6 +98,7 @@ public class RibbonLoadBalancedRetryPolicyFactoryTest { LoadBalancedRetryContext context = new LoadBalancedRetryContext(null, request); assertThat(policy.canRetryNextServer(context), is(true)); assertThat(policy.canRetrySameServer(context), is(false)); + assertThat(policy.retryableStatusCode(400), is(false)); } @Test @@ -112,6 +114,7 @@ public class RibbonLoadBalancedRetryPolicyFactoryTest { doReturn(nextServer).when(config).getPropertyAsInteger(eq(CommonClientConfigKey.MaxAutoRetriesNextServer), anyInt()); doReturn(retryOnAllOps).when(config).get(eq(CommonClientConfigKey.OkToRetryOnAllOperations), anyBoolean()); doReturn(retryOnAllOps).when(config).getPropertyAsBoolean(eq(CommonClientConfigKey.OkToRetryOnAllOperations), anyBoolean()); + doReturn("").when(config).getPropertyAsString(eq(RibbonLoadBalancedRetryPolicy.RETRYABLE_STATUS_CODES),eq("")); doReturn(server.getServiceId()).when(config).getClientName(); doReturn(config).when(clientFactory).getClientConfig(eq(server.getServiceId())); clientFactory.getLoadBalancerContext(server.getServiceId()).setRetryHandler(new DefaultLoadBalancerRetryHandler(config)); @@ -123,6 +126,7 @@ public class RibbonLoadBalancedRetryPolicyFactoryTest { LoadBalancedRetryContext context = new LoadBalancedRetryContext(null, request); assertThat(policy.canRetryNextServer(context), is(false)); assertThat(policy.canRetrySameServer(context), is(false)); + assertThat(policy.retryableStatusCode(400), is(false)); } @Test @@ -138,6 +142,7 @@ public class RibbonLoadBalancedRetryPolicyFactoryTest { doReturn(nextServer).when(config).getPropertyAsInteger(eq(CommonClientConfigKey.MaxAutoRetriesNextServer), anyInt()); doReturn(retryOnAllOps).when(config).get(eq(CommonClientConfigKey.OkToRetryOnAllOperations), anyBoolean()); doReturn(retryOnAllOps).when(config).getPropertyAsBoolean(eq(CommonClientConfigKey.OkToRetryOnAllOperations), anyBoolean()); + doReturn("").when(config).getPropertyAsString(eq(RibbonLoadBalancedRetryPolicy.RETRYABLE_STATUS_CODES),eq("")); doReturn(server.getServiceId()).when(config).getClientName(); doReturn(config).when(clientFactory).getClientConfig(eq(server.getServiceId())); clientFactory.getLoadBalancerContext(server.getServiceId()).initWithNiwsConfig(config); @@ -149,6 +154,7 @@ public class RibbonLoadBalancedRetryPolicyFactoryTest { LoadBalancedRetryContext context = new LoadBalancedRetryContext(null, request); assertThat(policy.canRetryNextServer(context), is(true)); assertThat(policy.canRetrySameServer(context), is(true)); + assertThat(policy.retryableStatusCode(400), is(false)); } @Test @@ -161,6 +167,7 @@ public class RibbonLoadBalancedRetryPolicyFactoryTest { doReturn(nextServer).when(config).get(eq(CommonClientConfigKey.MaxAutoRetriesNextServer), anyInt()); doReturn(false).when(config).get(eq(CommonClientConfigKey.OkToRetryOnAllOperations), eq(false)); doReturn(config).when(clientFactory).getClientConfig(eq(server.getServiceId())); + doReturn("").when(config).getPropertyAsString(eq(RibbonLoadBalancedRetryPolicy.RETRYABLE_STATUS_CODES),eq("")); clientFactory.getLoadBalancerContext(server.getServiceId()).setRetryHandler(new DefaultLoadBalancerRetryHandler(config)); RibbonLoadBalancerClient client = getRibbonLoadBalancerClient(server); RibbonLoadBalancedRetryPolicyFactory factory = new RibbonLoadBalancedRetryPolicyFactory(clientFactory); @@ -189,9 +196,32 @@ public class RibbonLoadBalancedRetryPolicyFactoryTest { } } assertThat(context.isExhaustedOnly(), is(true)); + assertThat(policy.retryableStatusCode(400), is(false)); verify(context, times(4)).setServiceInstance(any(ServiceInstance.class)); } + @Test + public void testRetryableStatusCodest() throws Exception { + int sameServer = 3; + int nextServer = 3; + RibbonServer server = getRibbonServer(); + IClientConfig config = mock(IClientConfig.class); + doReturn(sameServer).when(config).get(eq(CommonClientConfigKey.MaxAutoRetries), anyInt()); + doReturn(nextServer).when(config).get(eq(CommonClientConfigKey.MaxAutoRetriesNextServer), anyInt()); + doReturn(false).when(config).get(eq(CommonClientConfigKey.OkToRetryOnAllOperations), eq(false)); + doReturn(config).when(clientFactory).getClientConfig(eq(server.getServiceId())); + doReturn("404,502,foo, ,").when(config).getPropertyAsString(eq(RibbonLoadBalancedRetryPolicy.RETRYABLE_STATUS_CODES),eq("")); + clientFactory.getLoadBalancerContext(server.getServiceId()).setRetryHandler(new DefaultLoadBalancerRetryHandler(config)); + RibbonLoadBalancerClient client = getRibbonLoadBalancerClient(server); + RibbonLoadBalancedRetryPolicyFactory factory = new RibbonLoadBalancedRetryPolicyFactory(clientFactory); + LoadBalancedRetryPolicy policy = factory.create(server.getServiceId(), client); + HttpRequest request = mock(HttpRequest.class); + doReturn(HttpMethod.GET).when(request).getMethod(); + assertThat(policy.retryableStatusCode(400), is(false)); + assertThat(policy.retryableStatusCode(404), is(true)); + assertThat(policy.retryableStatusCode(502), is(true)); + } + protected RibbonLoadBalancerClient getRibbonLoadBalancerClient( RibbonServer ribbonServer) { given(this.loadBalancer.getName()).willReturn(ribbonServer.getServiceId()); diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClientTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClientTests.java index fa3b04a6..266b39be 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClientTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClientTests.java @@ -19,6 +19,7 @@ package org.springframework.cloud.netflix.ribbon.apache; import java.io.IOException; import java.net.URI; import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpUriRequest; @@ -29,6 +30,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration; +import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryPolicy; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryPolicyFactory; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerContext; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; @@ -185,7 +187,7 @@ public class RibbonLoadBalancingHttpClientTests { private RetryableRibbonLoadBalancingHttpClient setupClientForRetry(int retriesNextServer, int retriesSameServer, boolean retryable, boolean retryOnAllOps, String serviceName, String host, int port, - HttpClient delegate, ILoadBalancer lb) throws Exception { + HttpClient delegate, ILoadBalancer lb, String statusCodes) throws Exception { ServerIntrospector introspector = mock(ServerIntrospector.class); RetryHandler retryHandler = new DefaultLoadBalancerRetryHandler(retriesSameServer, retriesNextServer, retryable); doReturn(new Server(host, port)).when(lb).chooseServer(eq(serviceName)); @@ -193,10 +195,12 @@ public class RibbonLoadBalancingHttpClientTests { clientConfig.set(CommonClientConfigKey.OkToRetryOnAllOperations, retryOnAllOps); clientConfig.set(CommonClientConfigKey.MaxAutoRetriesNextServer, retriesNextServer); clientConfig.set(CommonClientConfigKey.MaxAutoRetries, retriesSameServer); + clientConfig.set(RibbonLoadBalancedRetryPolicy.RETRYABLE_STATUS_CODES, statusCodes); clientConfig.setClientName(serviceName); RibbonLoadBalancerContext context = new RibbonLoadBalancerContext(lb, clientConfig, retryHandler); SpringClientFactory clientFactory = mock(SpringClientFactory.class); doReturn(context).when(clientFactory).getLoadBalancerContext(eq(serviceName)); + doReturn(clientConfig).when(clientFactory).getClientConfig(eq(serviceName)); LoadBalancedRetryPolicyFactory factory = new RibbonLoadBalancedRetryPolicyFactory(clientFactory); RetryableRibbonLoadBalancingHttpClient client = new RetryableRibbonLoadBalancingHttpClient(clientConfig, introspector, factory); client.setLoadBalancer(lb); @@ -217,10 +221,13 @@ public class RibbonLoadBalancingHttpClientTests { URI uri = new URI("http://" + host + ":" + port); HttpClient delegate = mock(HttpClient.class); final HttpResponse response = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + doReturn(200).when(statusLine).getStatusCode(); + doReturn(statusLine).when(response).getStatusLine(); doThrow(new IOException("boom")).doReturn(response).when(delegate).execute(any(HttpUriRequest.class)); ILoadBalancer lb = mock(ILoadBalancer.class); RetryableRibbonLoadBalancingHttpClient client = setupClientForRetry(retriesNextServer, retriesSameServer, retryable, retryOnAllOps, - serviceName, host, port, delegate, lb); + serviceName, host, port, delegate, lb, ""); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(uri).when(request).getURI(); doReturn(method).when(request).getMethod(); @@ -246,11 +253,14 @@ public class RibbonLoadBalancingHttpClientTests { URI uri = new URI("http://" + host + ":" + port); HttpClient delegate = mock(HttpClient.class); final HttpResponse response = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + doReturn(200).when(statusLine).getStatusCode(); + doReturn(statusLine).when(response).getStatusLine(); doThrow(new IOException("boom")).doThrow(new IOException("boom again")).doReturn(response). when(delegate).execute(any(HttpUriRequest.class)); ILoadBalancer lb = mock(ILoadBalancer.class); RetryableRibbonLoadBalancingHttpClient client = setupClientForRetry(retriesNextServer, retriesSameServer, retryable, retryOnAllOps, - serviceName, host, port, delegate, lb); + serviceName, host, port, delegate, lb, ""); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(uri).when(request).getURI(); doReturn(method).when(request).getMethod(); @@ -276,11 +286,14 @@ public class RibbonLoadBalancingHttpClientTests { URI uri = new URI("http://" + host + ":" + port); HttpClient delegate = mock(HttpClient.class); final HttpResponse response = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + doReturn(200).when(statusLine).getStatusCode(); + doReturn(statusLine).when(response).getStatusLine(); doThrow(new IOException("boom")).doThrow(new IOException("boom again")).doReturn(response). when(delegate).execute(any(HttpUriRequest.class)); ILoadBalancer lb = mock(ILoadBalancer.class); RetryableRibbonLoadBalancingHttpClient client = setupClientForRetry(retriesNextServer, retriesSameServer, retryable, retryOnAllOps, - serviceName, host, port, delegate, lb); + serviceName, host, port, delegate, lb, ""); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(method).when(request).getMethod(); doReturn(uri).when(request).getURI(); @@ -309,7 +322,7 @@ public class RibbonLoadBalancingHttpClientTests { when(delegate).execute(any(HttpUriRequest.class)); ILoadBalancer lb = mock(ILoadBalancer.class); RetryableRibbonLoadBalancingHttpClient client = setupClientForRetry(retriesNextServer, retriesSameServer, retryable, retryOnAllOps, - serviceName, host, port, delegate, lb); + serviceName, host, port, delegate, lb, ""); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(method).when(request).getMethod(); doReturn(uri).when(request).getURI(); @@ -326,6 +339,42 @@ public class RibbonLoadBalancingHttpClientTests { } } + @Test + public void testRetryOnStatusCode() throws Exception { + int retriesNextServer = 0; + int retriesSameServer = 1; + boolean retryable = true; + boolean retryOnAllOps = false; + String serviceName = "foo"; + String host = serviceName; + int port = 80; + HttpMethod method = HttpMethod.GET; + URI uri = new URI("http://" + host + ":" + port); + HttpClient delegate = mock(HttpClient.class); + final HttpResponse response = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + doReturn(200).when(statusLine).getStatusCode(); + doReturn(statusLine).when(response).getStatusLine(); + final HttpResponse fourOFourResponse = mock(HttpResponse.class); + StatusLine fourOFourStatusLine = mock(StatusLine.class); + doReturn(404).when(fourOFourStatusLine).getStatusCode(); + doReturn(fourOFourStatusLine).when(fourOFourResponse).getStatusLine(); + doReturn(fourOFourResponse).doReturn(response).when(delegate).execute(any(HttpUriRequest.class)); + ILoadBalancer lb = mock(ILoadBalancer.class); + RetryableRibbonLoadBalancingHttpClient client = setupClientForRetry(retriesNextServer, retriesSameServer, retryable, retryOnAllOps, + serviceName, host, port, delegate, lb, "404"); + RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); + doReturn(uri).when(request).getURI(); + doReturn(method).when(request).getMethod(); + doReturn(request).when(request).withNewUri(any(URI.class)); + HttpUriRequest uriRequest = mock(HttpUriRequest.class); + doReturn(uri).when(uriRequest).getURI(); + doReturn(uriRequest).when(request).toRequest(any(RequestConfig.class)); + RibbonApacheHttpResponse returnedResponse = client.execute(request, null); + verify(delegate, times(2)).execute(any(HttpUriRequest.class)); + verify(lb, times(0)).chooseServer(eq(serviceName)); + } + @Configuration protected static class UseDefaults { @@ -395,8 +444,12 @@ public class RibbonLoadBalancingHttpClientTests { ReflectionTestUtils.setField(client, "delegate", delegate); ReflectionTestUtils.setField(client, "lb", loadBalancer); + HttpResponse httpResponse = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + doReturn(200).when(statusLine).getStatusCode(); + doReturn(statusLine).when(httpResponse).getStatusLine(); given(delegate.execute(any(HttpUriRequest.class))).willReturn( - mock(HttpResponse.class)); + httpResponse); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(uri).when(request).getURI(); doReturn(request).when(request).withNewUri(any(URI.class)); diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeExceptionTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeExceptionTest.java new file mode 100644 index 00000000..078774e5 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeExceptionTest.java @@ -0,0 +1,36 @@ +/* + * + * * 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.netflix.ribbon.support; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + + +/** + * @author Ryan Baxter + */ +public class RetryableStatusCodeExceptionTest { + + @Test + public void testMessage() { + RetryableStatusCodeException ex = new RetryableStatusCodeException("foo", 404); + assertEquals("Service foo returned a status code of 404", ex.getMessage()); + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonRetryIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonRetryIntegrationTests.java index 7dfea5a7..4002420a 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonRetryIntegrationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonRetryIntegrationTests.java @@ -47,7 +47,8 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; "disableretry.ribbon.MaxAutoRetriesNextServer: 1", "zuul.routes.globalretrydisabled: /globalretrydisabled/**", "globalretrydisabled.ribbon.MaxAutoRetries: 1", - "globalretrydisabled.ribbon.MaxAutoRetriesNextServer: 1" + "globalretrydisabled.ribbon.MaxAutoRetriesNextServer: 1", + "retryable.ribbon.retryableStatusCodes: 404,403" }) @DirtiesContext public class HttpClientRibbonRetryIntegrationTests extends RibbonRetryIntegrationTestBase { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonRetryIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonRetryIntegrationTests.java index 858ddc84..e8c39e3d 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonRetryIntegrationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonRetryIntegrationTests.java @@ -36,6 +36,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; "zuul.routes.retryable: /retryable/**", "zuul.routes.retryable.retryable: true", "retryable.ribbon.OkToRetryOnAllOperations: true", + "retryable.ribbon.retryableStatusCodes: 404", "retryable.ribbon.MaxAutoRetries: 1", "retryable.ribbon.MaxAutoRetriesNextServer: 1", "zuul.routes.getretryable: /getretryable/**", diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/RibbonRetryIntegrationTestBase.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/RibbonRetryIntegrationTestBase.java index ffdbd833..fb569716 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/RibbonRetryIntegrationTestBase.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/RibbonRetryIntegrationTestBase.java @@ -25,8 +25,15 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; +import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.cloud.netflix.ribbon.RibbonClients; +import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryPolicy; +import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryPolicyFactory; +import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerContext; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.ribbon.StaticServerList; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; import org.springframework.context.annotation.Bean; @@ -37,6 +44,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import com.netflix.loadbalancer.Server; @@ -74,6 +82,15 @@ public abstract class RibbonRetryIntegrationTestBase { assertEquals(HttpStatus.OK, result.getStatusCode()); } + @Test + public void retryableFourOFour() { + String uri = "/retryable/404everyothererror"; + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + uri, HttpMethod.GET, + new HttpEntity<>((Void) null), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + } + @Test public void postRetryOK() { String uri = "/retryable/posteveryothererror"; @@ -161,6 +178,17 @@ public abstract class RibbonRetryIntegrationTestBase { return timeout(); } + @RequestMapping("/404everyothererror") + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity fourOFourError() { + boolean shouldError = error; + error = !error; + if(shouldError) { + return new ResponseEntity("not found", HttpStatus.NOT_FOUND); + } + return new ResponseEntity("no error", HttpStatus.OK); + } + } @Configuration @@ -173,6 +201,46 @@ public abstract class RibbonRetryIntegrationTestBase { public ServerList ribbonServerList() { return new StaticServerList<>(new Server("localhost", this.port)); } + } + @Configuration + public static class FourOFourRetryableRibbonConfiguration extends RibbonClientConfiguration { + + @Bean + public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory(SpringClientFactory factory) { + return new MyRibbonRetryPolicyFactory(factory); + } + + public static class MyRibbonRetryPolicyFactory extends RibbonLoadBalancedRetryPolicyFactory { + + private SpringClientFactory factory; + + public MyRibbonRetryPolicyFactory(SpringClientFactory clientFactory) { + super(clientFactory); + this.factory = clientFactory; + } + + @Override + public LoadBalancedRetryPolicy create(String serviceId, ServiceInstanceChooser loadBalanceChooser) { + RibbonLoadBalancerContext lbContext = this.factory + .getLoadBalancerContext(serviceId); + return new MyLoadBalancedRetryPolicy(serviceId, lbContext, loadBalanceChooser); + } + + class MyLoadBalancedRetryPolicy extends RibbonLoadBalancedRetryPolicy { + + public MyLoadBalancedRetryPolicy(String serviceId, RibbonLoadBalancerContext context, ServiceInstanceChooser loadBalanceChooser) { + super(serviceId, context, loadBalanceChooser); + } + + @Override + public boolean retryableStatusCode( int statusCode) { + if(statusCode == HttpStatus.NOT_FOUND.value()) { + return true; + } + return super.retryableStatusCode(statusCode); + } + } + } } }