diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index f9961236..64c9c7fc 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -605,6 +605,14 @@ One of the main benefits of Hystrix is the set of metrics it gathers about each .Hystrix Dashboard image::Hystrix.png[] +== Hystrix Timeouts And Ribbon Clients + +When using Hystrix commands that wrap Ribbon clients you want to make sure your Hystrix timeout +is configured to be longer than the configured Ribbon timeout, including any potential +retries that might be made. For example, if your Ribbon connection timeout is one second and +the Ribbon client might retry the request three times, than your Hystrix timeout should +be slightly more than three seconds. + [[netflix-hystrix-dashboard-starter]] === How to Include Hystrix Dashboard @@ -1589,7 +1597,8 @@ If you are using `@EnableZuulProxy` with tha Spring Boot Actuator you will enable (by default) an additional endpoint, available via HTTP as `/routes`. A GET to this endpoint will return a list of the mapped routes. A POST will force a refresh of the existing routes (e.g. in -case there have been changes in the service catalog). +case there have been changes in the service catalog). You can disable +this endpoint by setting `endpoints.routes.enabled` to `false`. NOTE: the routes should respond automatically to changes in the service catalog, but the POST to /routes is a way to force the change @@ -1893,6 +1902,10 @@ Route filters: * `SimpleHostRoutingFilter`: This filter sends requests to predetermined URLs via an Apache HttpClient. URLs are found in `RequestContext.getRouteHost()`. +==== Custom Zuul Filter examples + +Most of the following "How to Write" examples below are included https://github.com/spring-cloud-samples/sample-zuul-filters[Sample Zuul Filters] project. There are also examples of manipulating the request or response body in that repository. + ==== How to Write a Pre Filter Pre filters are used to set up data in the `RequestContext` for use in filters downstream. The main use case is to set information required for route filters. @@ -2448,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/FeignClientsRegistrar.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java index 0375b5a7..12315a9d 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java @@ -248,7 +248,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, private String getUrl(Map attributes) { String url = resolve((String) attributes.get("url")); - if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.endsWith("}"))) { + if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) { if (!url.contains("://")) { url = "http://" + url; } 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/apache/RibbonApacheHttpRequest.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java index 2c1cb96c..44a8768b 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java @@ -63,10 +63,11 @@ public class RibbonApacheHttpRequest extends ContextAwareRequest implements Clon entity.setContent(this.context.getRequestEntity()); // if the entity contentLength isn't set, transfer-encoding will be set // to chunked in org.apache.http.protocol.RequestContent. See gh-1042 - if (this.context.getContentLength() != null) { - entity.setContentLength(this.context.getContentLength()); - } else if ("GET".equals(this.context.getMethod())) { + Long contentLength = this.context.getContentLength(); + if ("GET".equals(this.context.getMethod()) && (contentLength == null || contentLength < 0)) { entity.setContentLength(0); + } else if (contentLength != null) { + entity.setContentLength(contentLength); } builder.setEntity(entity); } 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/main/java/org/springframework/cloud/netflix/zuul/RoutesEndpoint.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesEndpoint.java index 920cb114..39b688e8 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesEndpoint.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesEndpoint.java @@ -20,74 +20,43 @@ import java.util.LinkedHashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.netflix.zuul.filters.Route; import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.jmx.export.annotation.ManagedAttribute; -import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; /** - * Endpoint to display and reset the zuul proxy routes + * Endpoint to display the zuul proxy routes * * @author Spencer Gibb * @author Dave Syer + * @author Ryan Baxter */ -@ManagedResource(description = "Can be used to list and reset the reverse proxy routes") -public class RoutesEndpoint implements MvcEndpoint, ApplicationEventPublisherAware { +@ManagedResource(description = "Can be used to list the reverse proxy routes") +@ConfigurationProperties(prefix = "endpoints.routes") +public class RoutesEndpoint extends AbstractEndpoint> { + + private static final String ID = "routes"; private RouteLocator routes; private ApplicationEventPublisher publisher; - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { - this.publisher = publisher; - } - @Autowired public RoutesEndpoint(RouteLocator routes) { + super(ID, true); this.routes = routes; } - @RequestMapping(method = RequestMethod.POST) - @ResponseBody - @ManagedOperation - public Map reset() { - this.publisher.publishEvent(new RoutesRefreshedEvent(this.routes)); - return getRoutes(); - } - - @RequestMapping(method = RequestMethod.GET) - @ResponseBody @ManagedAttribute - public Map getRoutes() { + public Map invoke() { Map map = new LinkedHashMap<>(); for (Route route : this.routes.getRoutes()) { map.put(route.getFullPath(), route.getLocation()); } return map; } - - @Override - public String getPath() { - return "/routes"; - } - - @Override - public boolean isSensitive() { - return true; - } - - @Override - public Class> getEndpointType() { - return null; - } - } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpoint.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpoint.java new file mode 100644 index 00000000..17e05640 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpoint.java @@ -0,0 +1,58 @@ +/* + * + * * 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.zuul; + +import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter; +import org.springframework.cloud.netflix.zuul.filters.RouteLocator; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Endpoint used to reset the reverse proxy routes + * @author Ryan Baxter + */ +@ManagedResource(description = "Can be used to reset the reverse proxy routes") +public class RoutesMvcEndpoint extends EndpointMvcAdapter implements ApplicationEventPublisherAware { + + private RouteLocator routes; + private ApplicationEventPublisher publisher; + + public RoutesMvcEndpoint(RoutesEndpoint endpoint, RouteLocator routes) { + super(endpoint); + this.routes = routes; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } + + @RequestMapping(method = RequestMethod.POST) + @ResponseBody + @ManagedOperation + public Object reset() { + this.publisher.publishEvent(new RoutesRefreshedEvent(this.routes)); + return super.invoke(); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java index d9ce3d3f..84e2a380 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java @@ -20,6 +20,8 @@ import java.util.Collection; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.metrics.CounterService; +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.ConditionalOnProperty; @@ -43,6 +45,9 @@ import org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter; import org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter; import org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter; import org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter; +import org.springframework.cloud.netflix.zuul.metrics.DefaultCounterFactory; +import org.springframework.cloud.netflix.zuul.metrics.EmptyCounterFactory; +import org.springframework.cloud.netflix.zuul.metrics.EmptyTracerFactory; import org.springframework.cloud.netflix.zuul.web.ZuulController; import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping; import org.springframework.context.ApplicationEvent; @@ -53,8 +58,12 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.context.event.ContextRefreshedEvent; +import com.netflix.zuul.FilterLoader; import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.filters.FilterRegistry; import com.netflix.zuul.http.ZuulServlet; +import com.netflix.zuul.monitoring.CounterFactory; +import com.netflix.zuul.monitoring.TracerFactory; /** * @author Spencer Gibb @@ -178,8 +187,39 @@ public class ZuulConfiguration { private Map filters; @Bean - public ZuulFilterInitializer zuulFilterInitializer() { - return new ZuulFilterInitializer(this.filters); + public ZuulFilterInitializer zuulFilterInitializer( + CounterFactory counterFactory, TracerFactory tracerFactory) { + FilterLoader filterLoader = FilterLoader.getInstance(); + FilterRegistry filterRegistry = FilterRegistry.instance(); + return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry); + } + + } + + @Configuration + @ConditionalOnClass(CounterService.class) + protected static class ZuulCounterFactoryConfiguration { + + @Bean + @ConditionalOnBean(CounterService.class) + public CounterFactory counterFactory(CounterService counterService) { + return new DefaultCounterFactory(counterService); + } + } + + @Configuration + protected static class ZuulMetricsConfiguration { + + @Bean + @ConditionalOnMissingBean(CounterFactory.class) + public CounterFactory counterFactory() { + return new EmptyCounterFactory(); + } + + @ConditionalOnMissingBean(TracerFactory.class) + @Bean + public TracerFactory tracerFactory() { + return new EmptyTracerFactory(); } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulFilterInitializer.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulFilterInitializer.java index 7b93e17d..a15f0d1f 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulFilterInitializer.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulFilterInitializer.java @@ -22,13 +22,15 @@ import java.util.Map; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; -import lombok.extern.apachecommons.CommonsLog; import org.springframework.util.ReflectionUtils; import com.netflix.zuul.FilterLoader; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.filters.FilterRegistry; -import com.netflix.zuul.monitoring.MonitoringHelper; +import com.netflix.zuul.monitoring.CounterFactory; +import com.netflix.zuul.monitoring.TracerFactory; + +import lombok.extern.apachecommons.CommonsLog; /** * @author Spencer Gibb @@ -38,10 +40,22 @@ import com.netflix.zuul.monitoring.MonitoringHelper; @CommonsLog public class ZuulFilterInitializer implements ServletContextListener { - private Map filters; + private final Map filters; + private final CounterFactory counterFactory; + private final TracerFactory tracerFactory; + private final FilterLoader filterLoader; + private final FilterRegistry filterRegistry; - public ZuulFilterInitializer(Map filters) { + public ZuulFilterInitializer(Map filters, + CounterFactory counterFactory, + TracerFactory tracerFactory, + FilterLoader filterLoader, + FilterRegistry filterRegistry) { this.filters = filters; + this.counterFactory = counterFactory; + this.tracerFactory = tracerFactory; + this.filterLoader = filterLoader; + this.filterRegistry = filterRegistry; } @Override @@ -49,32 +63,31 @@ public class ZuulFilterInitializer implements ServletContextListener { log.info("Starting filter initializer context listener"); - // FIXME: mocks monitoring infrastructure as we don't need it for this simple app - MonitoringHelper.initMocks(); - - FilterRegistry registry = FilterRegistry.instance(); + TracerFactory.initialize(tracerFactory); + CounterFactory.initialize(counterFactory); for (Map.Entry entry : this.filters.entrySet()) { - registry.put(entry.getKey(), entry.getValue()); + filterRegistry.put(entry.getKey(), entry.getValue()); } } @Override public void contextDestroyed(ServletContextEvent sce) { log.info("Stopping filter initializer context listener"); - FilterRegistry registry = FilterRegistry.instance(); for (Map.Entry entry : this.filters.entrySet()) { - registry.remove(entry.getKey()); + filterRegistry.remove(entry.getKey()); } clearLoaderCache(); + + TracerFactory.initialize(null); + CounterFactory.initialize(null); } private void clearLoaderCache() { - FilterLoader instance = FilterLoader.getInstance(); Field field = ReflectionUtils.findField(FilterLoader.class, "hashFiltersByType"); ReflectionUtils.makeAccessible(field); @SuppressWarnings("rawtypes") - Map cache = (Map) ReflectionUtils.getField(field, instance); + Map cache = (Map) ReflectionUtils.getField(field, filterLoader); cache.clear(); } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java index da22bee4..4557906f 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java @@ -139,6 +139,11 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { return new RoutesEndpoint(routeLocator); } + @Bean + public RoutesMvcEndpoint zuulMvcEndpoint(RouteLocator routeLocator, RoutesEndpoint endpoint) { + return new RoutesMvcEndpoint(endpoint, routeLocator); + } + @Bean public ProxyRequestHelper proxyRequestHelper(ZuulProperties zuulProperties) { TraceProxyRequestHelper helper = new TraceProxyRequestHelper(); diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java index 18d25915..483599f2 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java @@ -199,7 +199,7 @@ public class SimpleRouteLocator implements RouteLocator, Ordered { // do nothing } - log.debug("adjustedPath=" + path); + log.debug("adjustedPath=" + adjustedPath); return adjustedPath; } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java index 79c2e72c..e5c6e343 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java @@ -21,36 +21,24 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer; +import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - /** * @author Spencer Gibb */ -@Data -@RequiredArgsConstructor -@AllArgsConstructor public class RibbonCommandContext { - @NonNull private final String serviceId; - @NonNull private final String method; - @NonNull private final String uri; private final Boolean retryable; - @NonNull private final MultiValueMap headers; - @NonNull private final MultiValueMap params; private final InputStream requestEntity; - @NonNull private final List requestCustomizers; private Long contentLength; @@ -65,6 +53,34 @@ public class RibbonCommandContext { new ArrayList(), null); } + public RibbonCommandContext(String serviceId, String method, String uri, + Boolean retryable, MultiValueMap headers, + MultiValueMap params, InputStream requestEntity, + List requestCustomizers) { + this(serviceId, method, uri, retryable, headers, params, requestEntity, requestCustomizers, null); + } + + public RibbonCommandContext(String serviceId, String method, String uri, + Boolean retryable, MultiValueMap headers, + MultiValueMap params, InputStream requestEntity, + List requestCustomizers, Long contentLength) { + Assert.notNull(serviceId, "serviceId may not be null"); + Assert.notNull(method, "method may not be null"); + Assert.notNull(uri, "uri may not be null"); + Assert.notNull(headers, "headers may not be null"); + Assert.notNull(params, "params may not be null"); + Assert.notNull(requestCustomizers, "requestCustomizers may not be null"); + this.serviceId = serviceId; + this.method = method; + this.uri = uri; + this.retryable = retryable; + this.headers = headers; + this.params = params; + this.requestEntity = requestEntity; + this.requestCustomizers = requestCustomizers; + this.contentLength = contentLength; + } + public URI uri() { try { return new URI(this.uri); @@ -83,4 +99,81 @@ public class RibbonCommandContext { public String getVerb() { return this.method; } + + public String getServiceId() { + return serviceId; + } + + public String getMethod() { + return method; + } + + public String getUri() { + return uri; + } + + public Boolean getRetryable() { + return retryable; + } + + public MultiValueMap getHeaders() { + return headers; + } + + public MultiValueMap getParams() { + return params; + } + + public InputStream getRequestEntity() { + return requestEntity; + } + + public List getRequestCustomizers() { + return requestCustomizers; + } + + public Long getContentLength() { + return contentLength; + } + + public void setContentLength(Long contentLength) { + this.contentLength = contentLength; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RibbonCommandContext that = (RibbonCommandContext) o; + return Objects.equals(serviceId, that.serviceId) && + Objects.equals(method, that.method) && + Objects.equals(uri, that.uri) && + Objects.equals(retryable, that.retryable) && + Objects.equals(headers, that.headers) && + Objects.equals(params, that.params) && + Objects.equals(requestEntity, that.requestEntity) && + Objects.equals(requestCustomizers, that.requestCustomizers) && + Objects.equals(contentLength, that.contentLength); + } + + @Override + public int hashCode() { + return Objects.hash(serviceId, method, uri, retryable, headers, params, requestEntity, requestCustomizers, contentLength); + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("RibbonCommandContext{"); + sb.append("serviceId='").append(serviceId).append('\''); + sb.append(", method='").append(method).append('\''); + sb.append(", uri='").append(uri).append('\''); + sb.append(", retryable=").append(retryable); + sb.append(", headers=").append(headers); + sb.append(", params=").append(params); + sb.append(", requestEntity=").append(requestEntity); + sb.append(", requestCustomizers=").append(requestCustomizers); + sb.append(", contentLength=").append(contentLength); + sb.append('}'); + return sb.toString(); + } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java index 749e7462..66d17dcc 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java @@ -124,7 +124,7 @@ public class RibbonRoutingFilter extends ZuulFilter { .buildZuulRequestQueryParams(request); String verb = getVerb(request); InputStream requestEntity = getRequestBody(request); - if (request.getContentLength() < 0) { + if (request.getContentLength() < 0 && !verb.equalsIgnoreCase("GET")) { context.setChunkedRequestBody(); } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/metrics/DefaultCounterFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/metrics/DefaultCounterFactory.java new file mode 100644 index 00000000..e3a3ae6d --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/metrics/DefaultCounterFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2015 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.zuul.metrics; + +import org.springframework.boot.actuate.metrics.CounterService; + +import com.netflix.zuul.monitoring.CounterFactory; + +/** + * A counter based monitoring factory that uses {@link CounterService} to increment counters. + * + * @author Anastasiia Smirnova + */ +public class DefaultCounterFactory extends CounterFactory { + + private final CounterService counterService; + + public DefaultCounterFactory(CounterService counterService) { + this.counterService = counterService; + } + + @Override + public void increment(String name) { + counterService.increment(name); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/metrics/EmptyCounterFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/metrics/EmptyCounterFactory.java new file mode 100644 index 00000000..319502c8 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/metrics/EmptyCounterFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2015 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.zuul.metrics; + +import com.netflix.zuul.monitoring.CounterFactory; + +/** + * A counter based monitoring factory that does nothing. + * + * @author Anastasiia Smirnova + */ +public class EmptyCounterFactory extends CounterFactory { + @Override + public void increment(String name) { + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/metrics/EmptyTracerFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/metrics/EmptyTracerFactory.java new file mode 100644 index 00000000..e46bcbe5 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/metrics/EmptyTracerFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2015 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.zuul.metrics; + +import com.netflix.zuul.monitoring.Tracer; +import com.netflix.zuul.monitoring.TracerFactory; + +/** + * A time based monitoring factory that does nothing. + * + * @author Anastasiia Smirnova + */ +public class EmptyTracerFactory extends TracerFactory { + + private final EmptyTracer emptyTracer = new EmptyTracer(); + + @Override + public Tracer startMicroTracer(String name) { + return emptyTracer; + } + + private static final class EmptyTracer implements Tracer { + @Override + public void setName(String name) { + } + + @Override + public void stopAndLog() { + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignHttpClientUrlTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignHttpClientUrlTests.java index 9fd5e0ce..eabfd678 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignHttpClientUrlTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignHttpClientUrlTests.java @@ -88,13 +88,13 @@ public class FeignHttpClientUrlTests { Hello getHello(); } - @FeignClient(name = "beanappurl", url = "#{SERVER_URL}") + @FeignClient(name = "beanappurl", url = "#{SERVER_URL}path") protected interface BeanUrlClient { @RequestMapping(method = RequestMethod.GET, value = "/hello") Hello getHello(); } - @FeignClient(name = "beanappurlnoprotocol", url = "#{SERVER_URL_NO_PROTOCOL}") + @FeignClient(name = "beanappurlnoprotocol", url = "#{SERVER_URL_NO_PROTOCOL}path") protected interface BeanUrlClientNoProtocol { @RequestMapping(method = RequestMethod.GET, value = "/hello") Hello getHello(); @@ -113,6 +113,11 @@ public class FeignHttpClientUrlTests { return new Hello("hello world 1"); } + @RequestMapping(method = RequestMethod.GET, value = "/path/hello") + public Hello getHelloWithPath() { + return getHello(); + } + @Bean(name="SERVER_URL") public String serverUrl() { return "http://localhost:" + port + "/"; diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientRetryTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientRetryTests.java index e8f0d794..28a0ad36 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientRetryTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientRetryTests.java @@ -138,9 +138,9 @@ class LocalRibbonClientConfiguration { @Bean public ServerList ribbonServerList() { - return new StaticServerList<>(new Server("___mybadhost__", 10001), - new Server("___mybadhost2__", 10002), - new Server("___mybadhost3__", 10003), new Server("localhost", this.port)); + return new StaticServerList<>(new Server("mybadhost", 80), + new Server("mybadhost2", 10002), + new Server("mybadhost3", 10003), new Server("localhost", this.port)); } } 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/ContextPathZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ContextPathZuulProxyApplicationTests.java index 04974a41..25987b9e 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ContextPathZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ContextPathZuulProxyApplicationTests.java @@ -61,7 +61,7 @@ public class ContextPathZuulProxyApplicationTests { private DiscoveryClientRouteLocator routes; @Autowired - private RoutesEndpoint endpoint; + private RoutesMvcEndpoint endpoint; @Before public void setTestRequestContext() { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointIntegrationTests.java new file mode 100644 index 00000000..b5a54c60 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointIntegrationTests.java @@ -0,0 +1,85 @@ +/* + * + * * 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.zuul; + +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.RestController; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Ryan Baxter + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + value = {"zuul.routes.sslservice.url=https://localhost:8443", "management.security.enabled=false"}) +@DirtiesContext +public class RoutesEndpointIntegrationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private SimpleZuulProxyApplication.RoutesRefreshListener refreshListener; + + @Test + public void getRoutesTest() { + Map routes = restTemplate.getForObject("/admin/routes", Map.class); + assertEquals("https://localhost:8443", routes.get("/sslservice/**")); + } + + @Test + public void postRoutesTest() { + Map routes = restTemplate.postForObject("/admin/routes", null, Map.class); + assertEquals("https://localhost:8443", routes.get("/sslservice/**")); + assertTrue(refreshListener.wasCalled()); + } + + @Configuration + @EnableAutoConfiguration + @RestController + @EnableZuulProxy + static class SimpleZuulProxyApplication { + @Component + static class RoutesRefreshListener implements ApplicationListener { + private boolean called = false; + @Override + public void onApplicationEvent(RoutesRefreshedEvent routesRefreshedEvent) { + called = true; + } + + public boolean wasCalled() { + return called; + } + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointTests.java new file mode 100644 index 00000000..08340e14 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointTests.java @@ -0,0 +1,86 @@ +/* + * + * * 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.zuul; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.springframework.cloud.netflix.zuul.filters.Route; +import org.springframework.cloud.netflix.zuul.filters.RouteLocator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Ryan Baxter + */ +public class RoutesEndpointTests { + + private RouteLocator locator; + + @Before + public void setUp() { + this.locator = new RouteLocator() { + @Override + public Collection getIgnoredPaths() { + return null; + } + + @Override + public List getRoutes() { + List routes = new ArrayList<>(); + routes.add(new Route("foo", "foopath", "foolocation", null, true, Collections.EMPTY_SET)); + routes.add(new Route("bar", "barpath", "barlocation", null, true, Collections.EMPTY_SET)); + return routes; + } + + @Override + public Route getMatchingRoute(String path) { + return null; + } + }; + } + + @Test + public void testInvoke() { + RoutesEndpoint endpoint = new RoutesEndpoint(locator); + Map result = new HashMap(); + for(Route r : locator.getRoutes()) { + result.put(r.getFullPath(), r.getLocation()); + } + assertEquals(result , endpoint.invoke()); + } + + @Test + public void testId() { + RoutesEndpoint endpoint = new RoutesEndpoint(locator); + assertEquals("routes", endpoint.getId()); + } + + @Test + public void testIsSensitive() { + RoutesEndpoint endpoint = new RoutesEndpoint(locator); + assertTrue(endpoint.isSensitive()); + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpointTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpointTests.java new file mode 100644 index 00000000..e59ebfec --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpointTests.java @@ -0,0 +1,91 @@ +/* + * + * * 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.zuul; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.netflix.zuul.filters.Route; +import org.springframework.cloud.netflix.zuul.filters.RouteLocator; +import org.springframework.context.ApplicationEventPublisher; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Ryan Baxter + */ +@SpringBootTest +@RunWith(MockitoJUnitRunner.class) +public class RoutesMvcEndpointTests { + private RouteLocator locator; + private RoutesEndpoint endpoint; + @Mock + private ApplicationEventPublisher publisher; + + @Before + public void setUp() { + this.locator = new RouteLocator() { + @Override + public Collection getIgnoredPaths() { + return null; + } + + @Override + public List getRoutes() { + List routes = new ArrayList<>(); + routes.add(new Route("foo", "foopath", "foolocation", null, true, Collections.EMPTY_SET)); + routes.add(new Route("bar", "barpath", "barlocation", null, true, Collections.EMPTY_SET)); + return routes; + } + + @Override + public Route getMatchingRoute(String path) { + return null; + } + }; + endpoint = spy(new RoutesEndpoint(locator)); + } + + @Test + public void reset() throws Exception { + RoutesMvcEndpoint mvcEndpoint = new RoutesMvcEndpoint(endpoint, locator); + mvcEndpoint.setApplicationEventPublisher(publisher); + Map result = new HashMap(); + for(Route r : locator.getRoutes()) { + result.put(r.getFullPath(), r.getLocation()); + } + assertEquals(result , mvcEndpoint.reset()); + verify(endpoint, times(1)).invoke(); + verify(publisher, times(1)).publishEvent(isA(RoutesRefreshedEvent.class)); + } + +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ServletPathZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ServletPathZuulProxyApplicationTests.java index f625d5c8..bef8a490 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ServletPathZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ServletPathZuulProxyApplicationTests.java @@ -66,7 +66,7 @@ public class ServletPathZuulProxyApplicationTests { private DiscoveryClientRouteLocator routes; @Autowired - private RoutesEndpoint endpoint; + private RoutesMvcEndpoint endpoint; @Before public void setTestRequestContext() { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SimpleZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SimpleZuulProxyApplicationTests.java index 7712a636..768c6e6b 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SimpleZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SimpleZuulProxyApplicationTests.java @@ -66,7 +66,7 @@ public class SimpleZuulProxyApplicationTests { private DiscoveryClientRouteLocator routes; @Autowired - private RoutesEndpoint endpoint; + private RoutesMvcEndpoint endpoint; @Before public void setTestRequestContext() { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulFilterInitializerTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulFilterInitializerTests.java new file mode 100644 index 00000000..0abb2458 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulFilterInitializerTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013-2017 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.zuul; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletContextEvent; + +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.ReflectionUtils; + +import com.netflix.zuul.FilterLoader; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.filters.FilterRegistry; +import com.netflix.zuul.monitoring.CounterFactory; +import com.netflix.zuul.monitoring.TracerFactory; + +import org.junit.Test; + +import java.lang.reflect.Constructor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class ZuulFilterInitializerTests { + + private static final ServletContextEvent DUMMY_SERVLET_CONTEXT_EVENT = mock( + ServletContextEvent.class); + + private Map filters = getFilters(); + private CounterFactory counterFactory = mock(CounterFactory.class); + private TracerFactory tracerFactory = mock(TracerFactory.class); + private FilterLoader filterLoader = new FilterLoader(); + private FilterRegistry filterRegistry = getFilterRegistry(); + + private final ZuulFilterInitializer initializer = new ZuulFilterInitializer(filters, + counterFactory, tracerFactory, filterLoader, filterRegistry); + + @Test + public void shouldSetupOnContextInitializedEvent() throws Exception { + initializer.contextInitialized(DUMMY_SERVLET_CONTEXT_EVENT); + + assertEquals(tracerFactory, TracerFactory.instance()); + assertEquals(counterFactory, CounterFactory.instance()); + assertThat(filterRegistry.getAllFilters()) + .containsAll(filters.values()); + } + + @Test + public void shouldCleanupOnContextDestroyed() throws Exception { + initializer.contextDestroyed(DUMMY_SERVLET_CONTEXT_EVENT); + + assertEquals(null, ReflectionTestUtils.getField(TracerFactory.class, "INSTANCE")); + assertEquals(null, + ReflectionTestUtils.getField(CounterFactory.class, "INSTANCE")); + assertTrue(FilterRegistry.instance().getAllFilters().isEmpty()); + assertTrue(getHashFiltersByType().isEmpty()); + } + + private Map getHashFiltersByType() { + Field field = ReflectionUtils.findField(FilterLoader.class, "hashFiltersByType"); + ReflectionUtils.makeAccessible(field); + return (Map) ReflectionUtils.getField(field, FilterLoader.getInstance()); + } + + private Map getFilters() { + Map filters = new HashMap<>(); + filters.put("key1", mock(ZuulFilter.class)); + filters.put("key2", mock(ZuulFilter.class)); + return filters; + } + + private FilterRegistry getFilterRegistry() { + try { + Constructor constructor = FilterRegistry.class + .getDeclaredConstructor(new Class[0]); + constructor.setAccessible(true); + return constructor.newInstance(new Object[0]); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java new file mode 100644 index 00000000..e3db36c8 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2013-2017 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.zuul; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.netflix.ribbon.RibbonClient; +import org.springframework.cloud.netflix.ribbon.StaticServerList; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.ServerList; +import com.netflix.zuul.context.RequestContext; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = ZuulProxyApplicationTests.ZuulProxyApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT, properties = { + "zuul.routes.simple:/simple/**", "logging.level.org.apache.http: DEBUG" }) +@DirtiesContext +public class ZuulProxyApplicationTests { + + @LocalServerPort + private int port; + + @Before + public void setTestRequestcontext() { + RequestContext context = new RequestContext(); + RequestContext.testSetCurrentContext(context); + } + + @Test + public void getHasCorrectTransferEncoding() { + ResponseEntity result = new TestRestTemplate().getForEntity( + "http://localhost:" + this.port + "/simple/transferencoding", String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("missing", result.getBody()); + } + + @Test + public void postHasCorrectTransferEncoding() { + ResponseEntity result = new TestRestTemplate().postForEntity( + "http://localhost:" + this.port + "/simple/transferencoding", new HttpEntity<>("hello"), + String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("missing", result.getBody()); + } + + // Don't use @SpringBootApplication because we don't want to component scan + @Configuration + @EnableAutoConfiguration + @RestController + @EnableZuulProxy + @RibbonClient(name = "simple", configuration = TestRibbonClientConfiguration.class) + static class ZuulProxyApplication { + + @RequestMapping(value = "/transferencoding", method = RequestMethod.GET) + public String get(@RequestHeader(name = "Transfer-Encoding", required = false) String transferEncoding) { + if (transferEncoding == null) { + return "missing"; + } + return transferEncoding; + } + + @RequestMapping(value = "/transferencoding", method = RequestMethod.POST) + public String post(@RequestHeader(name = "Transfer-Encoding", required = false) String transferEncoding, + @RequestBody String hello) { + if (transferEncoding == null) { + return "missing"; + } + return transferEncoding; + } + + } + + // Load balancer with fixed server list for "simple" pointing to localhost + @Configuration + static class TestRibbonClientConfiguration { + + @LocalServerPort + private int port; + + @Bean + public ServerList ribbonServerList() { + return new StaticServerList<>(new Server("localhost", this.port)); + } + + } +} + diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CustomHostRoutingFilterTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CustomHostRoutingFilterTests.java index 8911e076..66982903 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CustomHostRoutingFilterTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CustomHostRoutingFilterTests.java @@ -23,7 +23,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; -import org.springframework.cloud.netflix.zuul.RoutesEndpoint; +import org.springframework.cloud.netflix.zuul.RoutesMvcEndpoint; import org.springframework.cloud.netflix.zuul.ZuulProxyConfiguration; import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator; import org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter; @@ -59,7 +59,7 @@ public class CustomHostRoutingFilterTests { private DiscoveryClientRouteLocator routes; @Autowired - private RoutesEndpoint endpoint; + private RoutesMvcEndpoint endpoint; @Before public void setTestRequestcontext() { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/discovery/PatternServiceRouteMapperIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/discovery/PatternServiceRouteMapperIntegrationTests.java index aa320df0..650b76a2 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/discovery/PatternServiceRouteMapperIntegrationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/discovery/PatternServiceRouteMapperIntegrationTests.java @@ -22,7 +22,7 @@ import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.cloud.netflix.ribbon.StaticServerList; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; -import org.springframework.cloud.netflix.zuul.RoutesEndpoint; +import org.springframework.cloud.netflix.zuul.RoutesMvcEndpoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpEntity; @@ -59,7 +59,7 @@ public class PatternServiceRouteMapperIntegrationTests { private DiscoveryClientRouteLocator routes; @Autowired - private RoutesEndpoint endpoint; + private RoutesMvcEndpoint endpoint; @Before public void setTestRequestcontext() { 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); + } + } + } } } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/ZuulProxyTestBase.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/ZuulProxyTestBase.java index e15aa557..3d2553bd 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/ZuulProxyTestBase.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/ZuulProxyTestBase.java @@ -37,7 +37,7 @@ import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorContro import org.springframework.boot.autoconfigure.web.servlet.error.ErrorAttributes; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.cloud.netflix.ribbon.StaticServerList; -import org.springframework.cloud.netflix.zuul.RoutesEndpoint; +import org.springframework.cloud.netflix.zuul.RoutesMvcEndpoint; import org.springframework.cloud.netflix.zuul.filters.Route; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator; @@ -88,7 +88,7 @@ public abstract class ZuulProxyTestBase { protected DiscoveryClientRouteLocator routes; @Autowired - protected RoutesEndpoint endpoint; + protected RoutesMvcEndpoint endpoint; @Autowired protected RibbonCommandFactory ribbonCommandFactory; diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/metrics/DefaultCounterFactoryTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/metrics/DefaultCounterFactoryTests.java new file mode 100644 index 00000000..2ba1f1b7 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/metrics/DefaultCounterFactoryTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2015 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.zuul.metrics; + +import org.springframework.boot.actuate.metrics.CounterService; + +import com.netflix.zuul.monitoring.CounterFactory; + +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class DefaultCounterFactoryTests { + + private static final String NAME = "my-super-metric-name"; + private final CounterService counterService = mock(CounterService.class); + private final CounterFactory factory = new DefaultCounterFactory(counterService); + + @Test + public void shouldIncrement() throws Exception { + factory.increment(NAME); + + verify(counterService).increment(NAME); + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/metrics/ZuulEmptyMetricsApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/metrics/ZuulEmptyMetricsApplicationTests.java new file mode 100644 index 00000000..e9f30573 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/metrics/ZuulEmptyMetricsApplicationTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2015 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.zuul.metrics; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.cloud.ClassPathExclusions; +import org.springframework.cloud.netflix.zuul.ZuulConfiguration; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.netflix.zuul.monitoring.CounterFactory; +import com.netflix.zuul.monitoring.TracerFactory; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +@ClassPathExclusions({ "spring-boot-starter-actuator-*.jar", + "spring-boot-actuator-*.jar" }) +public class ZuulEmptyMetricsApplicationTests { + + private AnnotationConfigApplicationContext context; + + @Before + public void setUp() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ZuulEmptyMetricsApplicationTestsConfiguration.class, + ZuulConfiguration.class); + context.refresh(); + + this.context = context; + } + + @After + public void tearDown() throws Exception { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void shouldSetupDefaultCounterFactoryIfCounterServiceIsPresent() + throws Exception { + CounterFactory factory = this.context.getBean(CounterFactory.class); + + assertEquals(EmptyCounterFactory.class, factory.getClass()); + } + + @Test + public void shouldSetupEmptyTracerFactory() throws Exception { + TracerFactory factory = this.context.getBean(TracerFactory.class); + + assertEquals(EmptyTracerFactory.class, factory.getClass()); + } + + @Configuration + static class ZuulEmptyMetricsApplicationTestsConfiguration { + + @Bean + ServerProperties serverProperties() { + return new ServerProperties(); + } + + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/metrics/ZuulMetricsApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/metrics/ZuulMetricsApplicationTests.java new file mode 100644 index 00000000..3520cbb1 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/metrics/ZuulMetricsApplicationTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2013-2015 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.zuul.metrics; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.netflix.zuul.EnableZuulServer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import com.netflix.zuul.exception.ZuulException; +import com.netflix.zuul.monitoring.CounterFactory; +import com.netflix.zuul.monitoring.TracerFactory; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { + ZuulMetricsApplicationTests.ZuulMetricsApplicationTestsConfiguration.class, + ZuulMetricsApplicationTests.ZuulConfig.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext +public class ZuulMetricsApplicationTests { + + private static final Map counters = new HashMap<>(); + + @Autowired + CounterFactory counterFactory; + @Autowired + TracerFactory tracerFactory; + + @Test + public void shouldSetupDefaultCounterFactoryIfCounterServiceIsPresent() + throws Exception { + assertEquals(DefaultCounterFactory.class, counterFactory.getClass()); + } + + @Test + public void shouldSetupEmptyTracerFactory() throws Exception { + assertEquals(EmptyTracerFactory.class, tracerFactory.getClass()); + } + + @Test + public void shouldIncrementCounters() throws Exception { + new ZuulException("any", 500, "cause"); + new ZuulException("any", 500, "cause"); + + assertEquals((long) counters.get("ZUUL::EXCEPTION:cause:500"), 2L); + + new ZuulException("any", 404, "cause2"); + new ZuulException("any", 404, "cause2"); + new ZuulException("any", 404, "cause2"); + + assertEquals((long) counters.get("ZUUL::EXCEPTION:cause2:404"), 3L); + } + + // Don't use @SpringBootApplication because we don't want to component scan + @Configuration + @EnableAutoConfiguration + @EnableZuulServer + static class ZuulConfig { + + } + + @Configuration + static class ZuulMetricsApplicationTestsConfiguration { + + @Bean + public ServerProperties serverProperties() { + return new ServerProperties(); + } + + @Bean + public CounterService counterService() { + return new CounterService() { + // not thread safe, but we are ok with it in tests + @Override + public void increment(String metricName) { + Long counter = counters.get(metricName); + if (counter == null) { + counter = 0L; + } + counters.put(metricName, ++counter); + } + + @Override + public void decrement(String metricName) { + } + + @Override + public void reset(String metricName) { + } + }; + } + } +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaDiscoveryClient.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaDiscoveryClient.java index 9a4768f6..2ea849f9 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaDiscoveryClient.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaDiscoveryClient.java @@ -27,6 +27,7 @@ import java.util.Map; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.util.Assert; import com.netflix.appinfo.EurekaInstanceConfig; import com.netflix.appinfo.InstanceInfo; @@ -102,7 +103,8 @@ public class EurekaDiscoveryClient implements DiscoveryClient { public static class EurekaServiceInstance implements ServiceInstance { private InstanceInfo instance; - EurekaServiceInstance(InstanceInfo instance) { + public EurekaServiceInstance(InstanceInfo instance) { + Assert.notNull(instance, "Service instance required"); this.instance = instance; } diff --git a/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.css b/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.css index 15e95ec8..c3111743 100644 --- a/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.css +++ b/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.css @@ -126,6 +126,10 @@ color: red; } +.badRequest { + color: #00CC99; +} + .dependencies .rejected { color: purple; } diff --git a/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuit.html b/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuit.html index 7132a497..e9328ab8 100644 --- a/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuit.html +++ b/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuit.html @@ -17,6 +17,7 @@ diff --git a/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/css/monitor.css b/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/css/monitor.css index a38647f0..04b929c2 100644 --- a/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/css/monitor.css +++ b/spring-cloud-netflix-hystrix-dashboard/src/main/resources/static/hystrix/css/monitor.css @@ -93,6 +93,10 @@ h3.sectionHeader { color: brown; } +.badRequest { + color: #00CC99; +} + @media screen and (max-width: 1100px) { .container { padding-left: 5px; diff --git a/spring-cloud-netflix-hystrix-dashboard/src/main/resources/templates/hystrix/monitor.ftl b/spring-cloud-netflix-hystrix-dashboard/src/main/resources/templates/hystrix/monitor.ftl index 24607e1d..b4f3edb0 100644 --- a/spring-cloud-netflix-hystrix-dashboard/src/main/resources/templates/hystrix/monitor.ftl +++ b/spring-cloud-netflix-hystrix-dashboard/src/main/resources/templates/hystrix/monitor.ftl @@ -54,7 +54,7 @@ 99.5