diff --git a/config/releaser.yml b/config/releaser.yml index 20b9f16a..26e5e8c2 100644 --- a/config/releaser.yml +++ b/config/releaser.yml @@ -1,3 +1,3 @@ releaser: maven: - buildCommand: ./scripts/build.sh + buildCommand: ./scripts/build.sh {{systemProps}} diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 33840a1e..8d5a72ee 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -669,8 +669,9 @@ in order for the Hystrix Dashboard to make a successful connection to the stream Looking at an individual instances Hystrix data is not very useful in terms of the overall health of the system. https://github.com/Netflix/Turbine[Turbine] is an application that aggregates all of the relevant `/hystrix.stream` endpoints into a combined `/turbine.stream` for use in the Hystrix Dashboard. Individual instances are located via Eureka. Running Turbine is as simple as annotating your main class with the `@EnableTurbine` annotation (e.g. using spring-cloud-starter-netflix-turbine to set up the classpath). All of the documented configuration properties from https://github.com/Netflix/Turbine/wiki/Configuration-(1.x)[the Turbine 1 wiki] apply. The only difference is that the `turbine.instanceUrlSuffix` does not need the port prepended as this is handled automatically unless `turbine.instanceInsertPort=false`. -NOTE: By default, Turbine looks for the `/hystrix.stream` endpoint on a registered instance by looking up its `homePageUrl` entry in Eureka, then appending `/hystrix.stream` to it. This means that if `spring-boot-actuator` is running on its own port (which is the default), the call to `/hystrix.stream` will fail. -To make turbine find the Hystrix stream at the correct port, you need to add `management.port` to the instances' metadata: +NOTE: By default, Turbine looks for the `/hystrix.stream` endpoint on a registered instance by looking up its `hostName` and `port` entries in Eureka, then appending `/hystrix.stream` to it. +If the instance's metadata contains `management.port`, it will be used instead of the `port` value for the `/hystrix.stream` endpoint. +By default, metadata entry `management.port` is equal to the `management.port` configuration property, it can be overridden though with following configuration: ---- eureka: instance: @@ -955,7 +956,33 @@ zuul: threadPoolKeyPrefix: zuulgw ---- +[[how-to-provdie-a-key-to-ribbon]] +=== How to Provide a Key to Ribbon's `IRule` +If you need to provide your own `IRule` implementation to handle a special routing requirement like a canary test, +you probably want to pass some information to the `choose` method of `IRule`. + +.com.netflix.loadbalancer.IRule.java +---- +public interface IRule{ + public Server choose(Object key); + : +---- + +You can provide some information that will be used to choose a target server by your `IRule` implementation like +the following: + +---- +RequestContext.getCurrentContext() + .set(FilterConstants.LOAD_BALANCER_KEY, "canary-test"); +---- + +If you put any object into the `RequestContext` with a key `FilterConstants.LOAD_BALANCER_KEY`, it will +be passed to the `choose` method of `IRule` implementation. Above code must be executed before `RibbonRoutingFilter` +is executed and Zuul's pre filter is the best place to do that. You can easily access HTTP headers and query parameters +via `RequestContext` in pre filter, so it can be used to determine `LOAD_BALANCER_KEY` that will be passed to Ribbon. +If you don't put any value with `LOAD_BALANCER_KEY` in `RequestContext`, null will be passed as a parameter of `choose` +method. [[spring-cloud-feign]] == Declarative REST Client: Feign @@ -1247,7 +1274,7 @@ protected interface HystrixClient { static class HystrixClientFallbackFactory implements FallbackFactory { @Override public HystrixClient create(Throwable cause) { - return new HystrixClientWithFallBackFactory() { + return new HystrixClient() { @Override public Hello iFailSometimes() { return new Hello("fallback; reason was: " + cause.getMessage()); @@ -2086,6 +2113,17 @@ class MyFallbackProvider implements FallbackProvider { } ---- +=== Zuul Timeouts + +If you want to configure the socket timeouts and read timeouts for requests proxied through +Zuul there are two options based on your configuration. + +If Zuul is using service discovery than you need to configure these timeouts via Ribbon properties, +`ribbon.ReadTimeout` and `ribbon.SocketTimeout`. + +If you have configured Zuul routes by specifying URLs than you will need to use +`zuul.host.connect-timeout-millis` and `zuul.host.socket-timeout-millis`. + [[zuul-redirect-location-rewrite]] === Rewriting `Location` header @@ -2694,6 +2732,27 @@ https://github.com/spring-projects/spring-retry[Spring Retry] on your applicatio When Spring Retry is present load balanced `RestTemplates`, Feign, and Zuul will automatically retry any failed requests (assuming you configuration allows it to). +==== BackOff Policies +By default no backoff policy is used when retrying requests. If you would like to configure +a backoff policy you will need to create a bean of type `LoadBalancedBackOffPolicyFactory` +which will be used to create a `BackOffPolicy` for a given service. + +[source,java,indent=0] +---- +@Configuration +public class MyConfiguration { + @Bean + LoadBalancedBackOffPolicyFactory backOffPolciyFactory() { + return new LoadBalancedBackOffPolicyFactory() { + @Override + public BackOffPolicy createBackOffPolicy(String service) { + return new ExponentialBackOffPolicy(); + } + }; + } +} +---- + ==== Configuration Anytime Ribbon is used with Spring Retry you can control the retry functionality by configuring diff --git a/scripts/build.sh b/scripts/build.sh index 2dae9e91..fe43ccda 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,4 +1,4 @@ #!/bin/bash -(cd spring-cloud-netflix-hystrix-contract && ../mvnw clean install) -./mvnw clean install +(cd spring-cloud-netflix-hystrix-contract && ../mvnw clean install -B -Pdocs ${@}) +./mvnw clean install -B -Pdocs ${@} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/CachingSpringLoadBalancerFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/CachingSpringLoadBalancerFactory.java index c2e0f940..3930a663 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/CachingSpringLoadBalancerFactory.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/CachingSpringLoadBalancerFactory.java @@ -18,6 +18,7 @@ package org.springframework.cloud.netflix.feign.ribbon; import java.util.Map; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancedRetryPolicyFactory; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; @@ -32,11 +33,13 @@ import com.netflix.loadbalancer.ILoadBalancer; * * @author Spencer Gibb * @author Dave Syer + * @author Ryan Baxter */ public class CachingSpringLoadBalancerFactory { private final SpringClientFactory factory; private final LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory; + private final LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory; private boolean enableRetry = false; private volatile Map cache = new ConcurrentReferenceHashMap<>(); @@ -44,19 +47,35 @@ public class CachingSpringLoadBalancerFactory { public CachingSpringLoadBalancerFactory(SpringClientFactory factory) { this.factory = factory; this.loadBalancedRetryPolicyFactory = new RibbonLoadBalancedRetryPolicyFactory(factory); + this.loadBalancedBackOffPolicyFactory = null; } + @Deprecated + //TODO remove in 2.0.x public CachingSpringLoadBalancerFactory(SpringClientFactory factory, LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory) { this.factory = factory; this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; + this.loadBalancedBackOffPolicyFactory = null; } + @Deprecated + //TODO remove in 2.0.0x public CachingSpringLoadBalancerFactory(SpringClientFactory factory, LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory, boolean enableRetry) { this.factory = factory; this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; this.enableRetry = enableRetry; + this.loadBalancedBackOffPolicyFactory = null; + } + + public CachingSpringLoadBalancerFactory(SpringClientFactory factory, + LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory, + LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory) { + this.factory = factory; + this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; + this.loadBalancedBackOffPolicyFactory = loadBalancedBackOffPolicyFactory; + this.enableRetry = true; } public FeignLoadBalancer create(String clientName) { @@ -67,7 +86,7 @@ public class CachingSpringLoadBalancerFactory { ILoadBalancer lb = this.factory.getLoadBalancer(clientName); ServerIntrospector serverIntrospector = this.factory.getInstance(clientName, ServerIntrospector.class); FeignLoadBalancer client = enableRetry ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector, - loadBalancedRetryPolicyFactory) : new FeignLoadBalancer(lb, config, serverIntrospector); + loadBalancedRetryPolicyFactory, loadBalancedBackOffPolicyFactory) : new FeignLoadBalancer(lb, config, serverIntrospector); this.cache.put(clientName, client); return client; } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java index 7d304986..c9ca5d33 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.netflix.feign.FeignAutoConfiguration; import org.springframework.cloud.netflix.feign.support.FeignHttpClientProperties; @@ -65,8 +66,9 @@ public class FeignRibbonClientAutoConfiguration { @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate") public CachingSpringLoadBalancerFactory retryabeCachingLBClientFactory( SpringClientFactory factory, - LoadBalancedRetryPolicyFactory retryPolicyFactory) { - return new CachingSpringLoadBalancerFactory(factory, retryPolicyFactory, true); + LoadBalancedRetryPolicyFactory retryPolicyFactory, + LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory) { + return new CachingSpringLoadBalancerFactory(factory, retryPolicyFactory, loadBalancedBackOffPolicyFactory); } @Bean 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 d82578ee..12546e2a 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 @@ -23,6 +23,7 @@ import feign.Response; import java.io.IOException; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; @@ -32,6 +33,8 @@ import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; +import org.springframework.retry.backoff.BackOffPolicy; +import org.springframework.retry.backoff.NoBackOffPolicy; import org.springframework.retry.policy.NeverRetryPolicy; import org.springframework.retry.support.RetryTemplate; import com.netflix.client.DefaultLoadBalancerRetryHandler; @@ -48,12 +51,26 @@ import com.netflix.loadbalancer.Server; public class RetryableFeignLoadBalancer extends FeignLoadBalancer implements ServiceInstanceChooser { private final LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory; + private final LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory; + @Deprecated + //TODO remove in 2.0.x public RetryableFeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig, ServerIntrospector serverIntrospector, LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory) { super(lb, clientConfig, serverIntrospector); this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; this.setRetryHandler(new DefaultLoadBalancerRetryHandler(clientConfig)); + this.loadBalancedBackOffPolicyFactory = new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory(); + } + + public RetryableFeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig, + ServerIntrospector serverIntrospector, LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory, + LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory) { + super(lb, clientConfig, serverIntrospector); + this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; + this.setRetryHandler(new DefaultLoadBalancerRetryHandler(clientConfig)); + this.loadBalancedBackOffPolicyFactory = loadBalancedBackOffPolicyFactory == null ? + new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory() : loadBalancedBackOffPolicyFactory; } @Override @@ -72,6 +89,8 @@ public class RetryableFeignLoadBalancer extends FeignLoadBalancer implements Ser } final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create(this.getClientName(), this); RetryTemplate retryTemplate = new RetryTemplate(); + BackOffPolicy backOffPolicy = loadBalancedBackOffPolicyFactory.createBackOffPolicy(this.getClientName()); + retryTemplate.setBackOffPolicy(backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy); retryTemplate.setRetryPolicy(retryPolicy == null ? new NeverRetryPolicy() : new FeignRetryPolicy(request.toHttpRequest(), retryPolicy, this, this.getClientName())); return retryTemplate.execute(new RetryCallback() { 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 1362387e..1c93c1d9 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 @@ -35,6 +35,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.actuator.HasFeatures; import org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; @@ -102,6 +104,13 @@ public class RibbonAutoConfiguration { return new LoadBalancedRetryPolicyFactory.NeverRetryFactory(); } + @Bean + @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate") + @ConditionalOnMissingBean + public LoadBalancedBackOffPolicyFactory loadBalancedBackoffPolicyFactory() { + return new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory(); + } + @Bean @ConditionalOnMissingBean public PropertiesFactory propertiesFactory() { diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/HttpClientRibbonConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/HttpClientRibbonConfiguration.java index 6f2b6c27..2eb8e41c 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/HttpClientRibbonConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/HttpClientRibbonConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory; import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory; @@ -151,9 +152,11 @@ public class HttpClientRibbonConfiguration { public RetryableRibbonLoadBalancingHttpClient retryableRibbonLoadBalancingHttpClient( IClientConfig config, ServerIntrospector serverIntrospector, ILoadBalancer loadBalancer, RetryHandler retryHandler, - LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory, CloseableHttpClient httpClient) { + LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory, CloseableHttpClient httpClient, + LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory) { RetryableRibbonLoadBalancingHttpClient client = new RetryableRibbonLoadBalancingHttpClient( - httpClient, config, serverIntrospector, loadBalancedRetryPolicyFactory); + httpClient, config, serverIntrospector, loadBalancedRetryPolicyFactory, + loadBalancedBackOffPolicyFactory); client.setLoadBalancer(loadBalancer); client.setRetryHandler(retryHandler); Monitors.registerObject("Client_" + this.name, client); 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 769c75bc..949343b7 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 @@ -24,6 +24,7 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; @@ -35,6 +36,8 @@ import org.springframework.cloud.netflix.ribbon.ServerIntrospector; import org.springframework.http.HttpRequest; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; +import org.springframework.retry.backoff.BackOffPolicy; +import org.springframework.retry.backoff.NoBackOffPolicy; import org.springframework.retry.policy.NeverRetryPolicy; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -51,7 +54,11 @@ import com.netflix.loadbalancer.Server; public class RetryableRibbonLoadBalancingHttpClient extends RibbonLoadBalancingHttpClient implements ServiceInstanceChooser { private LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory = new LoadBalancedRetryPolicyFactory.NeverRetryFactory(); + private LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory = + new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory(); + @Deprecated + //TODO remove in 2.0.x public RetryableRibbonLoadBalancingHttpClient(IClientConfig config, ServerIntrospector serverIntrospector, LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory) { @@ -59,6 +66,8 @@ public class RetryableRibbonLoadBalancingHttpClient extends RibbonLoadBalancingH this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; } + @Deprecated + //TODO remove in 2.0.x public RetryableRibbonLoadBalancingHttpClient(CloseableHttpClient delegate, IClientConfig config, ServerIntrospector serverIntrospector, LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory) { @@ -66,6 +75,15 @@ public class RetryableRibbonLoadBalancingHttpClient extends RibbonLoadBalancingH this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; } + public RetryableRibbonLoadBalancingHttpClient(CloseableHttpClient delegate, + IClientConfig config, ServerIntrospector serverIntrospector, + LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory, + LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory) { + super(delegate, config, serverIntrospector); + this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; + this.loadBalancedBackOffPolicyFactory = loadBalancedBackOffPolicyFactory; + } + @Override public RibbonApacheHttpResponse execute(final RibbonApacheHttpRequest request, final IClientConfig configOverride) throws Exception { @@ -137,6 +155,8 @@ public class RetryableRibbonLoadBalancingHttpClient extends RibbonLoadBalancingH retryTemplate.setRetryPolicy(retryPolicy == null || !retryable ? new NeverRetryPolicy() : new RetryPolicy(request, retryPolicy, this, this.getClientName())); + BackOffPolicy backOffPolicy = loadBalancedBackOffPolicyFactory.createBackOffPolicy(this.getClientName()); + retryTemplate.setBackOffPolicy(backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy); return retryTemplate.execute(callback); } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonConfiguration.java index 1380df83..1e1f2fbf 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonConfiguration.java @@ -34,6 +34,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; @@ -115,9 +116,10 @@ public class OkHttpRibbonConfiguration { ILoadBalancer loadBalancer, RetryHandler retryHandler, LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory, - OkHttpClient delegate) { + OkHttpClient delegate, + LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory) { RetryableOkHttpLoadBalancingClient client = new RetryableOkHttpLoadBalancingClient(delegate, config, - serverIntrospector, loadBalancedRetryPolicyFactory); + serverIntrospector, loadBalancedRetryPolicyFactory, loadBalancedBackOffPolicyFactory); client.setLoadBalancer(loadBalancer); client.setRetryHandler(retryHandler); Monitors.registerObject("Client_" + this.name, client); 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 3fd53528..67876d47 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 @@ -22,6 +22,7 @@ import okhttp3.Response; import java.net.URI; import org.apache.commons.lang.BooleanUtils; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; @@ -33,6 +34,8 @@ import org.springframework.cloud.netflix.ribbon.ServerIntrospector; import org.springframework.http.HttpRequest; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; +import org.springframework.retry.backoff.BackOffPolicy; +import org.springframework.retry.backoff.NoBackOffPolicy; import org.springframework.retry.policy.NeverRetryPolicy; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -48,17 +51,31 @@ import com.netflix.loadbalancer.Server; public class RetryableOkHttpLoadBalancingClient extends OkHttpLoadBalancingClient implements ServiceInstanceChooser { private LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory; + private LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory = + new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory(); + @Deprecated + //TODO remove in 2.0.x public RetryableOkHttpLoadBalancingClient(OkHttpClient delegate, IClientConfig config, ServerIntrospector serverIntrospector, LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory) { super(delegate, config, serverIntrospector); this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; } + public RetryableOkHttpLoadBalancingClient(OkHttpClient delegate, IClientConfig config, ServerIntrospector serverIntrospector, + LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory, + LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory) { + super(delegate, config, serverIntrospector); + this.loadBalancedRetryPolicyFactory = loadBalancedRetryPolicyFactory; + this.loadBalancedBackOffPolicyFactory = loadBalancedBackOffPolicyFactory; + } + private OkHttpRibbonResponse executeWithRetry(OkHttpRibbonRequest request, LoadBalancedRetryPolicy retryPolicy, RetryCallback callback) throws Exception { RetryTemplate retryTemplate = new RetryTemplate(); + BackOffPolicy backOffPolicy = loadBalancedBackOffPolicyFactory.createBackOffPolicy(this.getClientName()); + retryTemplate.setBackOffPolicy(backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy); boolean retryable = request.getContext() == null ? true : BooleanUtils.toBooleanDefaultIfNull(request.getContext().getRetryable(), true); retryTemplate.setRetryPolicy(retryPolicy == null || !retryable ? new NeverRetryPolicy() diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/AbstractLoadBalancingClient.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/AbstractLoadBalancingClient.java index 22659d30..a3988c66 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/AbstractLoadBalancingClient.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/AbstractLoadBalancingClient.java @@ -17,6 +17,7 @@ package org.springframework.cloud.netflix.ribbon.support; +import com.netflix.loadbalancer.reactive.LoadBalancerCommand; import org.springframework.cloud.netflix.ribbon.DefaultServerIntrospector; import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; @@ -140,4 +141,11 @@ public abstract class AbstractLoadBalancingClient builder) { + if (request.getLoadBalancerKey() != null) { + builder.withServerLocator(request.getLoadBalancerKey()); + } + } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java index 7d2929e2..428a406c 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java @@ -43,6 +43,7 @@ public abstract class ContextAwareRequest extends ClientRequest implements HttpR } this.uri = context.uri(); this.isRetriable = context.getRetryable(); + this.loadBalancerKey = context.getLoadBalancerKey(); } public RibbonCommandContext getContext() { @@ -73,7 +74,8 @@ public abstract class ContextAwareRequest extends ClientRequest implements HttpR RibbonCommandContext commandContext = new RibbonCommandContext(this.context.getServiceId(), this.context.getMethod(), uri.toString(), this.context.getRetryable(), this.context.getHeaders(), this.context.getParams(), this.context.getRequestEntity(), - this.context.getRequestCustomizers(), this.context.getContentLength()); + this.context.getRequestCustomizers(), this.context.getContentLength(), + this.context.getLoadBalancerKey()); return commandContext; } } 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 deleted file mode 100644 index afc857b5..00000000 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeException.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * - * * 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 - * @deprecated Use {@link org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException} instead - */ -//TODO Remove in Edgeware -@Deprecated -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/ribbon/support/RibbonCommandContext.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContext.java index 9732f522..a70ef713 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContext.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContext.java @@ -30,6 +30,7 @@ import java.util.Objects; /** * @author Spencer Gibb + * @author Yongsung Yoon */ public class RibbonCommandContext { private final String serviceId; @@ -41,6 +42,7 @@ public class RibbonCommandContext { private final List requestCustomizers; private InputStream requestEntity; private Long contentLength; + private Object loadBalancerKey; /** * Kept for backwards compatibility with Spring Cloud Sleuth 1.x versions @@ -50,7 +52,7 @@ public class RibbonCommandContext { String uri, Boolean retryable, MultiValueMap headers, MultiValueMap params, InputStream requestEntity) { this(serviceId, method, uri, retryable, headers, params, requestEntity, - new ArrayList(), null); + new ArrayList(), null, null); } public RibbonCommandContext(String serviceId, String method, String uri, @@ -58,13 +60,22 @@ public class RibbonCommandContext { MultiValueMap params, InputStream requestEntity, List requestCustomizers) { this(serviceId, method, uri, retryable, headers, params, requestEntity, - requestCustomizers, null); + requestCustomizers, null, null); } public RibbonCommandContext(String serviceId, String method, String uri, Boolean retryable, MultiValueMap headers, MultiValueMap params, InputStream requestEntity, List requestCustomizers, Long contentLength) { + this(serviceId, method, uri, retryable, headers, params, requestEntity, + requestCustomizers, contentLength, null); + } + + public RibbonCommandContext(String serviceId, String method, String uri, + Boolean retryable, MultiValueMap headers, + MultiValueMap params, InputStream requestEntity, + List requestCustomizers, Long contentLength, + Object loadBalancerKey) { Assert.notNull(serviceId, "serviceId may not be null"); Assert.notNull(method, "method may not be null"); Assert.notNull(uri, "uri may not be null"); @@ -80,6 +91,7 @@ public class RibbonCommandContext { this.requestEntity = requestEntity; this.requestCustomizers = requestCustomizers; this.contentLength = contentLength; + this.loadBalancerKey = loadBalancerKey; } public URI uri() { @@ -153,6 +165,14 @@ public class RibbonCommandContext { this.contentLength = contentLength; } + public Object getLoadBalancerKey() { + return loadBalancerKey; + } + + public void setLoadBalancerKey(Object loadBalancerKey) { + this.loadBalancerKey = loadBalancerKey; + } + @Override public boolean equals(Object o) { if (this == o) @@ -167,13 +187,14 @@ public class RibbonCommandContext { .equals(params, that.params) && Objects .equals(requestEntity, that.requestEntity) && Objects .equals(requestCustomizers, that.requestCustomizers) && Objects - .equals(contentLength, that.contentLength); + .equals(contentLength, that.contentLength) && Objects + .equals(loadBalancerKey, that.loadBalancerKey); } @Override public int hashCode() { return Objects.hash(serviceId, method, uri, retryable, headers, params, - requestEntity, requestCustomizers, contentLength); + requestEntity, requestCustomizers, contentLength, loadBalancerKey); } @Override @@ -188,6 +209,7 @@ public class RibbonCommandContext { sb.append(", requestEntity=").append(requestEntity); sb.append(", requestCustomizers=").append(requestCustomizers); sb.append(", contentLength=").append(contentLength); + sb.append(", loadBalancerKey=").append(loadBalancerKey); sb.append('}'); return sb.toString(); } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancerTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancerTests.java index 2a230da4..0149e30a 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancerTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/RetryableFeignLoadBalancerTests.java @@ -32,6 +32,7 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; @@ -42,6 +43,10 @@ import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerContext; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.http.HttpRequest; +import org.springframework.retry.RetryContext; +import org.springframework.retry.backoff.BackOffContext; +import org.springframework.retry.backoff.BackOffInterruptedException; +import org.springframework.retry.backoff.BackOffPolicy; import com.netflix.client.RequestSpecificRetryHandler; import com.netflix.client.config.CommonClientConfigKey; @@ -79,6 +84,8 @@ public class RetryableFeignLoadBalancerTests { @Mock private IClientConfig config; private ServerIntrospector inspector = new DefaultServerIntrospector(); + private LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory = + new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory(); private Integer defaultConnectTimeout = 10000; private Integer defaultReadTimeout = 10000; @@ -117,7 +124,8 @@ public class RetryableFeignLoadBalancerTests { FeignLoadBalancer.RibbonRequest request = new FeignLoadBalancer.RibbonRequest(client, feignRequest, new URI("http://foo")); Response response = Response.builder().status(200).headers(new HashMap>()).build(); doReturn(response).when(client).execute(any(Request.class), any(Request.Options.class)); - RetryableFeignLoadBalancer feignLb = new RetryableFeignLoadBalancer(lb, config, inspector, loadBalancedRetryPolicyFactory); + RetryableFeignLoadBalancer feignLb = new RetryableFeignLoadBalancer(lb, config, inspector, loadBalancedRetryPolicyFactory, + loadBalancedBackOffPolicyFactory); FeignLoadBalancer.RibbonResponse ribbonResponse = feignLb.execute(request, null); assertEquals(200, ribbonResponse.toResponse().status()); verify(client, times(1)).execute(any(Request.class), any(Request.Options.class)); @@ -136,7 +144,7 @@ public class RetryableFeignLoadBalancerTests { public LoadBalancedRetryPolicy create(String s, ServiceInstanceChooser serviceInstanceChooser) { return null; } - }); + }, loadBalancedBackOffPolicyFactory); try { feignLb.execute(request, null); } catch(Exception e) { @@ -167,10 +175,13 @@ public class RetryableFeignLoadBalancerTests { FeignLoadBalancer.RibbonRequest request = new FeignLoadBalancer.RibbonRequest(client, feignRequest, new URI("http://foo")); Response response = Response.builder().status(200).headers(new HashMap>()).build(); doThrow(new IOException("boom")).doReturn(response).when(client).execute(any(Request.class), any(Request.Options.class)); - RetryableFeignLoadBalancer feignLb = new RetryableFeignLoadBalancer(lb, config, inspector, loadBalancedRetryPolicyFactory); + MyBackOffPolicyFactory backOffPolicyFactory = new MyBackOffPolicyFactory(); + RetryableFeignLoadBalancer feignLb = new RetryableFeignLoadBalancer(lb, config, inspector, loadBalancedRetryPolicyFactory, + backOffPolicyFactory); FeignLoadBalancer.RibbonResponse ribbonResponse = feignLb.execute(request, null); assertEquals(200, ribbonResponse.toResponse().status()); verify(client, times(2)).execute(any(Request.class), any(Request.Options.class)); + assertEquals(1, backOffPolicyFactory.getCount()); } @Test @@ -195,10 +206,13 @@ public class RetryableFeignLoadBalancerTests { 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); + MyBackOffPolicyFactory backOffPolicyFactory = new MyBackOffPolicyFactory(); + RetryableFeignLoadBalancer feignLb = new RetryableFeignLoadBalancer(lb, config, inspector, loadBalancedRetryPolicyFactory, + backOffPolicyFactory); FeignLoadBalancer.RibbonResponse ribbonResponse = feignLb.execute(request, null); assertEquals(200, ribbonResponse.toResponse().status()); verify(client, times(2)).execute(any(Request.class), any(Request.Options.class)); + assertEquals(1, backOffPolicyFactory.getCount()); } @Test @@ -214,7 +228,8 @@ public class RetryableFeignLoadBalancerTests { FeignLoadBalancer.RibbonRequest request = new FeignLoadBalancer.RibbonRequest(client, feignRequest, new URI("http://foo")); Response response = Response.builder().status(200).headers(new HashMap>()).build(); doReturn(response).when(client).execute(any(Request.class), any(Request.Options.class)); - RetryableFeignLoadBalancer feignLb = new RetryableFeignLoadBalancer(lb, config, inspector, loadBalancedRetryPolicyFactory); + RetryableFeignLoadBalancer feignLb = new RetryableFeignLoadBalancer(lb, config, inspector, loadBalancedRetryPolicyFactory, + loadBalancedBackOffPolicyFactory); RequestSpecificRetryHandler retryHandler = feignLb.getRequestSpecificRetryHandler(request, config); assertEquals(1, retryHandler.getMaxRetriesOnNextServer()); assertEquals(1, retryHandler.getMaxRetriesOnSameServer()); @@ -265,11 +280,35 @@ public class RetryableFeignLoadBalancerTests { public List getAllServers() { return null; } - }, config, inspector, loadBalancedRetryPolicyFactory); + }, config, inspector, loadBalancedRetryPolicyFactory, loadBalancedBackOffPolicyFactory); ServiceInstance serviceInstance = feignLb.choose("foo"); assertEquals("foo", serviceInstance.getHost()); assertEquals(80, serviceInstance.getPort()); } + class MyBackOffPolicyFactory implements LoadBalancedBackOffPolicyFactory, BackOffPolicy { + + private int count = 0; + + @Override + public BackOffContext start(RetryContext retryContext) { + return null; + } + + @Override + public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException { + count++; + } + + public int getCount() { + return count; + } + + @Override + public BackOffPolicy createBackOffPolicy(String service) { + return this; + } + } + } \ No newline at end of file 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 ea836566..3a63a090 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 @@ -31,6 +31,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; import org.springframework.cloud.commons.httpclient.HttpClientConfiguration; import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration; @@ -43,6 +44,10 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; +import org.springframework.retry.RetryContext; +import org.springframework.retry.backoff.BackOffContext; +import org.springframework.retry.backoff.BackOffInterruptedException; +import org.springframework.retry.backoff.BackOffPolicy; import org.springframework.test.util.ReflectionTestUtils; import com.netflix.client.DefaultLoadBalancerRetryHandler; @@ -55,6 +60,7 @@ import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.Server; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.mockito.BDDMockito.given; @@ -74,6 +80,7 @@ import static org.mockito.Mockito.when; public class RibbonLoadBalancingHttpClientTests { private ILoadBalancer loadBalancer; + private LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory = new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory(); @Before public void setup() { @@ -198,9 +205,10 @@ public class RibbonLoadBalancingHttpClientTests { } private RetryableRibbonLoadBalancingHttpClient setupClientForRetry(int retriesNextServer, int retriesSameServer, - boolean retryable, boolean retryOnAllOps, - String serviceName, String host, int port, - HttpClient delegate, ILoadBalancer lb, String statusCodes) throws Exception { + boolean retryable, boolean retryOnAllOps, + String serviceName, String host, int port, + CloseableHttpClient delegate, ILoadBalancer lb, String statusCodes, + LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory) throws Exception { ServerIntrospector introspector = mock(ServerIntrospector.class); RetryHandler retryHandler = new DefaultLoadBalancerRetryHandler(retriesSameServer, retriesNextServer, retryable); doReturn(new Server(host, port)).when(lb).chooseServer(eq(serviceName)); @@ -215,7 +223,8 @@ public class RibbonLoadBalancingHttpClientTests { 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); + RetryableRibbonLoadBalancingHttpClient client = new RetryableRibbonLoadBalancingHttpClient(delegate, clientConfig, + introspector, factory, loadBalancedBackOffPolicyFactory); client.setLoadBalancer(lb); ReflectionTestUtils.setField(client, "delegate", delegate); return client; @@ -240,7 +249,7 @@ public class RibbonLoadBalancingHttpClientTests { 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, "", loadBalancedBackOffPolicyFactory); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(uri).when(request).getURI(); doReturn(method).when(request).getMethod(); @@ -272,8 +281,9 @@ public class RibbonLoadBalancingHttpClientTests { doThrow(new IOException("boom")).doThrow(new IOException("boom again")).doReturn(response). when(delegate).execute(any(HttpUriRequest.class)); ILoadBalancer lb = mock(ILoadBalancer.class); + MyBackOffPolicyFactory myBackOffPolicyFactory = new MyBackOffPolicyFactory(); RetryableRibbonLoadBalancingHttpClient client = setupClientForRetry(retriesNextServer, retriesSameServer, retryable, retryOnAllOps, - serviceName, host, port, delegate, lb, ""); + serviceName, host, port, delegate, lb, "", myBackOffPolicyFactory); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(uri).when(request).getURI(); doReturn(method).when(request).getMethod(); @@ -284,6 +294,7 @@ public class RibbonLoadBalancingHttpClientTests { RibbonApacheHttpResponse returnedResponse = client.execute(request, null); verify(delegate, times(3)).execute(any(HttpUriRequest.class)); verify(lb, times(1)).chooseServer(eq(serviceName)); + assertEquals(2, myBackOffPolicyFactory.getCount()); } @Test @@ -305,8 +316,9 @@ public class RibbonLoadBalancingHttpClientTests { doThrow(new IOException("boom")).doThrow(new IOException("boom again")).doReturn(response). when(delegate).execute(any(HttpUriRequest.class)); ILoadBalancer lb = mock(ILoadBalancer.class); + MyBackOffPolicyFactory myBackOffPolicyFactory = new MyBackOffPolicyFactory(); RetryableRibbonLoadBalancingHttpClient client = setupClientForRetry(retriesNextServer, retriesSameServer, retryable, retryOnAllOps, - serviceName, host, port, delegate, lb, ""); + serviceName, host, port, delegate, lb, "", myBackOffPolicyFactory); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(method).when(request).getMethod(); doReturn(uri).when(request).getURI(); @@ -317,6 +329,7 @@ public class RibbonLoadBalancingHttpClientTests { verify(response, times(0)).close(); verify(delegate, times(3)).execute(any(HttpUriRequest.class)); verify(lb, times(1)).chooseServer(eq(serviceName)); + assertEquals(2, myBackOffPolicyFactory.getCount()); } @Test @@ -336,7 +349,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, "", loadBalancedBackOffPolicyFactory); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(method).when(request).getMethod(); doReturn(uri).when(request).getURI(); @@ -376,8 +389,9 @@ public class RibbonLoadBalancingHttpClientTests { doReturn(fourOFourStatusLine).when(fourOFourResponse).getStatusLine(); doReturn(fourOFourResponse).doReturn(response).when(delegate).execute(any(HttpUriRequest.class)); ILoadBalancer lb = mock(ILoadBalancer.class); + MyBackOffPolicyFactory myBackOffPolicyFactory = new MyBackOffPolicyFactory(); RetryableRibbonLoadBalancingHttpClient client = setupClientForRetry(retriesNextServer, retriesSameServer, retryable, retryOnAllOps, - serviceName, host, port, delegate, lb, "404"); + serviceName, host, port, delegate, lb, "404", myBackOffPolicyFactory); RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); doReturn(uri).when(request).getURI(); doReturn(method).when(request).getMethod(); @@ -389,6 +403,7 @@ public class RibbonLoadBalancingHttpClientTests { verify(fourOFourResponse, times(1)).close(); verify(delegate, times(2)).execute(any(HttpUriRequest.class)); verify(lb, times(0)).chooseServer(eq(serviceName)); + assertEquals(1, myBackOffPolicyFactory.getCount()); } @Configuration @@ -480,4 +495,28 @@ public class RibbonLoadBalancingHttpClientTests { return requestConfigCaptor.getValue(); } + class MyBackOffPolicyFactory implements LoadBalancedBackOffPolicyFactory, BackOffPolicy { + + private int count = 0; + + @Override + public BackOffContext start(RetryContext retryContext) { + return null; + } + + @Override + public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException { + count++; + } + + public int getCount() { + return count; + } + + @Override + public BackOffPolicy createBackOffPolicy(String service) { + return this; + } + } + } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequestTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequestTests.java index 608ae59c..c4f28e9c 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequestTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequestTests.java @@ -56,6 +56,7 @@ public class ContextAwareRequestTests { doReturn(new URI("http://foo")).when(context).uri(); doReturn("foo").when(context).getServiceId(); doReturn(new LinkedMultiValueMap<>()).when(context).getParams(); + doReturn("testLoadBalancerKey").when(context).getLoadBalancerKey(); request = new TestContextAwareRequest(context); } @@ -97,6 +98,18 @@ public class ContextAwareRequestTests { assertEquals(headers, request.getHeaders()); } + @Test + public void getLoadBalancerKey() throws Exception { + assertEquals("testLoadBalancerKey", request.getLoadBalancerKey()); + + RibbonCommandContext defaultContext = mock(RibbonCommandContext.class); + doReturn(new LinkedMultiValueMap()).when(defaultContext).getHeaders(); + doReturn(null).when(defaultContext).getLoadBalancerKey(); + ContextAwareRequest defaultRequest = new TestContextAwareRequest(defaultContext); + + assertNull(defaultRequest.getLoadBalancerKey()); + } + static class TestContextAwareRequest extends ContextAwareRequest { public TestContextAwareRequest(RibbonCommandContext context) { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeExceptionTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeExceptionTests.java deleted file mode 100644 index e151e23c..00000000 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/RetryableStatusCodeExceptionTests.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * - * * 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 RetryableStatusCodeExceptionTests { - - @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/ribbon/support/RibbonCommandContextTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContextTest.java index 7c976b5a..dc571dad 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContextTest.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContextTest.java @@ -19,6 +19,7 @@ package org.springframework.cloud.netflix.ribbon.support; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Collections; import org.junit.Test; import org.springframework.http.HttpMethod; @@ -28,6 +29,7 @@ import com.google.common.collect.Lists; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import okhttp3.Request; @@ -92,4 +94,18 @@ public class RibbonCommandContextTest { new ByteArrayInputStream(TEST_CONTENT), Lists.newArrayList(requestCustomizer)); } + + @Test + public void testNullSafetyWithNullableParameters() throws Exception { + LinkedMultiValueMap headers = new LinkedMultiValueMap(); + LinkedMultiValueMap params = new LinkedMultiValueMap(); + + RibbonCommandContext testContext = new RibbonCommandContext("serviceId", + HttpMethod.POST.toString(), "/my/route", true, headers, params, + new ByteArrayInputStream(TEST_CONTENT), Collections.emptyList(), + null, null); + + assertNotEquals(0, testContext.hashCode()); + assertNotNull(testContext.toString()); + } } \ No newline at end of file diff --git a/spring-cloud-netflix-dependencies/pom.xml b/spring-cloud-netflix-dependencies/pom.xml index e990fe4c..e2d8e2d6 100644 --- a/spring-cloud-netflix-dependencies/pom.xml +++ b/spring-cloud-netflix-dependencies/pom.xml @@ -18,7 +18,7 @@ 1.8.4 9.5.1 1.5.12 - 2.2.2 + 2.2.4 0.10.1 1.3.0 1.2.0 @@ -301,6 +301,46 @@ feign-okhttp ${feign.version} + + io.github.openfeign + feign-gson + ${feign.version} + + + io.github.openfeign + feign-jackson-jaxb + ${feign.version} + + + io.github.openfeign + feign-jackson + ${feign.version} + + + io.github.openfeign + feign-java8 + ${feign.version} + + + io.github.openfeign + feign-jaxb + ${feign.version} + + + io.github.openfeign + feign-jaxrs + ${feign.version} + + + io.github.openfeign + feign-ribbon + ${feign.version} + + + io.github.openfeign + feign-sax + ${feign.version} + com.netflix.hystrix hystrix-core diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java index 784cb758..0d10f314 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java @@ -12,7 +12,6 @@ * 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.eureka; @@ -23,7 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.MalformedURLException; -import java.net.URL; +import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -51,6 +50,9 @@ import org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfi import org.springframework.cloud.commons.util.InetUtils; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.cloud.netflix.eureka.config.DiscoveryClientOptionalArgsConfiguration; +import org.springframework.cloud.netflix.eureka.metadata.DefaultManagementMetadataProvider; +import org.springframework.cloud.netflix.eureka.metadata.ManagementMetadata; +import org.springframework.cloud.netflix.eureka.metadata.ManagementMetadataProvider; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry; @@ -119,58 +121,76 @@ public class EurekaClientAutoConfiguration { return client; } + @Bean + @ConditionalOnMissingBean + public ManagementMetadataProvider serviceManagementMetadataProvider() { + return new DefaultManagementMetadataProvider(); + } + private String getProperty(String property) { return this.env.containsProperty(property) ? this.env.getProperty(property) : ""; } @Bean @ConditionalOnMissingBean(value = EurekaInstanceConfig.class, search = SearchStrategy.CURRENT) - public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils) { + public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils, + ManagementMetadataProvider managementMetadataProvider) { String hostname = getProperty("eureka.instance.hostname"); boolean preferIpAddress = Boolean.parseBoolean(getProperty("eureka.instance.prefer-ip-address")); boolean isSecurePortEnabled = Boolean.parseBoolean(getProperty("eureka.instance.secure-port-enabled")); - int nonSecurePort = Integer.valueOf(env.getProperty("server.port", env.getProperty("port", "8080"))); - int managementPort = Integer.valueOf(env.getProperty("management.port", String.valueOf(nonSecurePort))); - String managementContextPath = env.getProperty("management.context-path", env.getProperty("server.servlet.context-path", "/")); + String serverContextPath = env.getProperty("server.context-path", "/"); + int serverPort = Integer.valueOf(env.getProperty("server.port", env.getProperty("port", "8080"))); + + Integer managementPort = env.getProperty("management.server.port", Integer.class);// nullable. should be wrapped into optional + String managementContextPath = env.getProperty("management.server.context-path");// nullable. should be wrapped into optional + Integer jmxPort = env.getProperty("com.sun.management.jmxremote.port", Integer.class);//nullable EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils); - instance.setNonSecurePort(nonSecurePort); + + instance.setNonSecurePort(serverPort); instance.setInstanceId(getDefaultInstanceId(env)); instance.setPreferIpAddress(preferIpAddress); if(isSecurePortEnabled) { - int securePort = Integer.valueOf(env.getProperty("server.port", env.getProperty("port", "8080"))); - instance.setSecurePort(securePort); + instance.setSecurePort(serverPort); } - if (managementPort != nonSecurePort && managementPort != 0) { - if (StringUtils.hasText(hostname)) { - instance.setHostname(hostname); - } - String statusPageUrlPath = getProperty("eureka.instance.status-page-url-path"); - String healthCheckUrlPath = getProperty("eureka.instance.health-check-url-path"); - if (!managementContextPath.endsWith("/")) { - managementContextPath = managementContextPath + "/"; - } - if (StringUtils.hasText(statusPageUrlPath)) { - instance.setStatusPageUrlPath(statusPageUrlPath); - } - if (StringUtils.hasText(healthCheckUrlPath)) { - instance.setHealthCheckUrlPath(healthCheckUrlPath); - } + if (StringUtils.hasText(hostname)) { + instance.setHostname(hostname); + } + String statusPageUrlPath = getProperty("eureka.instance.status-page-url-path"); + String healthCheckUrlPath = getProperty("eureka.instance.health-check-url-path"); - String scheme = instance.getSecurePortEnabled() ? "https" : "http"; - try { - URL base = new URL(scheme, instance.getHostname(), managementPort, managementContextPath); - instance.setStatusPageUrl(new URL(base, StringUtils.trimLeadingCharacter(instance.getStatusPageUrlPath(), '/')).toString()); - instance.setHealthCheckUrl(new URL(base, StringUtils.trimLeadingCharacter(instance.getHealthCheckUrlPath(), '/')).toString()); - } catch (MalformedURLException e) { - log.error("Unable to set status page or health check url.", e); + if (StringUtils.hasText(statusPageUrlPath)) { + instance.setStatusPageUrlPath(statusPageUrlPath); + } + if (StringUtils.hasText(healthCheckUrlPath)) { + instance.setHealthCheckUrlPath(healthCheckUrlPath); + } + + ManagementMetadata metadata = managementMetadataProvider.get(instance, serverPort, + serverContextPath, managementContextPath, managementPort); + + if(metadata != null) { + instance.setStatusPageUrl(metadata.getStatusPageUrl()); + instance.setHealthCheckUrl(metadata.getHealthCheckUrl()); + Map metadataMap = instance.getMetadataMap(); + if (metadataMap.get("management.port") == null) { + metadataMap.put("management.port", String.valueOf(metadata.getManagementPort())); } } + + setupJmxPort(instance, jmxPort); return instance; } + private void setupJmxPort(EurekaInstanceConfigBean instance, Integer jmxPort) { + Map metadataMap = instance.getMetadataMap(); + if (metadataMap.get("jmx.port") == null && jmxPort != null) { + metadataMap.put("jmx.port", String.valueOf(jmxPort)); + } + } + @Bean public DiscoveryClient discoveryClient(EurekaInstanceConfig config, EurekaClient client) { return new EurekaDiscoveryClient(config, client); diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/metadata/DefaultManagementMetadataProvider.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/metadata/DefaultManagementMetadataProvider.java new file mode 100644 index 00000000..4f19b003 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/metadata/DefaultManagementMetadataProvider.java @@ -0,0 +1,97 @@ +package org.springframework.cloud.netflix.eureka.metadata; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; +import org.springframework.util.StringUtils; + +import java.net.MalformedURLException; +import java.net.URL; + +public class DefaultManagementMetadataProvider implements ManagementMetadataProvider { + + private static final int RANDOM_PORT = 0; + private static final Log log = LogFactory.getLog(DefaultManagementMetadataProvider.class); + + @Override + public ManagementMetadata get(EurekaInstanceConfigBean instance, int serverPort, + String serverContextPath, String managementContextPath, + Integer managementPort) { + if (isRandom(managementPort)) { + return null; + } + if (managementPort == null && isRandom(serverPort)) { + return null; + } + String healthCheckUrl = getHealthCheckUrl(instance, serverPort, serverContextPath, + managementContextPath, managementPort); + String statusPageUrl = getStatusPageUrl(instance, serverPort, serverContextPath, + managementContextPath, managementPort); + + return new ManagementMetadata(healthCheckUrl, statusPageUrl, managementPort == null ? serverPort : managementPort); + } + + private boolean isRandom(Integer port) { + return port != null && port == RANDOM_PORT; + } + + private String getHealthCheckUrl(EurekaInstanceConfigBean instance, int serverPort, String serverContextPath, + String managementContextPath, Integer managementPort) { + String healthCheckUrlPath = instance.getHealthCheckUrlPath(); + String healthCheckUrl = getUrl(instance, serverPort, serverContextPath, managementContextPath, + managementPort, healthCheckUrlPath); + log.debug("Constructed eureka meta-data healthcheckUrl: " + healthCheckUrl); + return healthCheckUrl; + } + + public String getStatusPageUrl(EurekaInstanceConfigBean instance, int serverPort, String serverContextPath, + String managementContextPath, Integer managementPort) { + String statusPageUrlPath = instance.getStatusPageUrlPath(); + String statusPageUrl = getUrl(instance, serverPort, serverContextPath, managementContextPath, + managementPort, statusPageUrlPath); + log.debug("Constructed eureka meta-data statusPageUrl: " + statusPageUrl); + return statusPageUrl; + } + + private String getUrl(EurekaInstanceConfigBean instance, int serverPort, + String serverContextPath, String managementContextPath, + Integer managementPort, String urlPath) { + managementContextPath = refineManagementContextPath(serverContextPath, managementContextPath, managementPort); + if (managementPort == null) { + managementPort = serverPort; + } + String scheme = instance.getSecurePortEnabled() ? "https" : "http"; + return constructValidUrl(scheme, instance.getHostname(), managementPort, managementContextPath, urlPath); + } + + private String refineManagementContextPath(String serverContextPath, String managementContextPath, + Integer managementPort) { + if(managementContextPath != null) { + return managementContextPath; + } + if(managementPort != null) { + return "/"; + } + return serverContextPath; + } + + private String constructValidUrl(String scheme, String hostname, int port, + String contextPath, String statusPath) { + try { + if (!contextPath.endsWith("/")) { + contextPath = contextPath + "/"; + } + URL base = new URL(scheme, hostname, port, contextPath); + String refinedStatusPath = StringUtils.trimLeadingCharacter(statusPath, '/'); + return new URL(base, refinedStatusPath).toString(); + } catch (MalformedURLException e) { + String message = getErrorMessage(scheme, hostname, port, contextPath, statusPath); + throw new IllegalStateException(message, e); + } + } + + private String getErrorMessage(String scheme, String hostname, int port, String contextPath, String statusPath) { + return String.format("Failed to construct url for scheme: %s, hostName: %s port: %s contextPath: %s statusPath: %s", + scheme, hostname, port, contextPath, statusPath); + } +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/metadata/ManagementMetadata.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/metadata/ManagementMetadata.java new file mode 100644 index 00000000..baa4a4ac --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/metadata/ManagementMetadata.java @@ -0,0 +1,53 @@ +package org.springframework.cloud.netflix.eureka.metadata; + +import java.util.Objects; + +public class ManagementMetadata { + + private final String healthCheckUrl; + private final String statusPageUrl; + private final Integer managementPort; + + public ManagementMetadata(String healthCheckUrl, String statusPageUrl, Integer managementPort) { + this.healthCheckUrl = healthCheckUrl; + this.statusPageUrl = statusPageUrl; + this.managementPort = managementPort; + } + + public String getHealthCheckUrl() { + return healthCheckUrl; + } + + public String getStatusPageUrl() { + return statusPageUrl; + } + + public Integer getManagementPort() { + return managementPort; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ManagementMetadata that = (ManagementMetadata) o; + return Objects.equals(healthCheckUrl, that.healthCheckUrl) && + Objects.equals(statusPageUrl, that.statusPageUrl) && + Objects.equals(managementPort, that.managementPort); + } + + @Override + public int hashCode() { + return Objects.hash(healthCheckUrl, statusPageUrl, managementPort); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ManagementMetadata{"); + sb.append("healthCheckUrl='").append(healthCheckUrl).append('\''); + sb.append(", statusPageUrl='").append(statusPageUrl).append('\''); + sb.append(", managementPort=").append(managementPort); + sb.append('}'); + return sb.toString(); + } +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/metadata/ManagementMetadataProvider.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/metadata/ManagementMetadataProvider.java new file mode 100644 index 00000000..98bc770e --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/metadata/ManagementMetadataProvider.java @@ -0,0 +1,10 @@ +package org.springframework.cloud.netflix.eureka.metadata; + +import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; + +public interface ManagementMetadataProvider { + + ManagementMetadata get(EurekaInstanceConfigBean instance, int serverPort, + String serverContextPath, String managementContextPath, Integer managementPort); + +} diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java index fc5d7aeb..87cfadd2 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java @@ -12,7 +12,6 @@ * 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.eureka; @@ -81,6 +80,54 @@ public class EurekaClientAutoConfigurationTests { this.context.refresh(); } + @Test + public void shouldSetManagementPortInMetadataMapIfEqualToServerPort() throws Exception { + addEnvironment(this.context, "server.port=8989"); + setupContext(RefreshAutoConfiguration.class); + + EurekaInstanceConfigBean instance = this.context + .getBean(EurekaInstanceConfigBean.class); + + assertEquals("8989", instance.getMetadataMap().get("management.port")); + } + + @Test + public void shouldNotSetManagementAndJmxPortsInMetadataMap() throws Exception { + addEnvironment(this.context, "server.port=8989", "management.port=0"); + setupContext(RefreshAutoConfiguration.class); + + EurekaInstanceConfigBean instance = this.context + .getBean(EurekaInstanceConfigBean.class); + + assertEquals(null, instance.getMetadataMap().get("management.port")); + assertEquals(null, instance.getMetadataMap().get("jmx.port")); + } + + @Test + public void shouldSetManagementAndJmxPortsInMetadataMap() throws Exception { + addEnvironment(this.context, "management.port=9999", + "com.sun.management.jmxremote.port=6789"); + setupContext(RefreshAutoConfiguration.class); + + EurekaInstanceConfigBean instance = this.context + .getBean(EurekaInstanceConfigBean.class); + assertEquals("9999", instance.getMetadataMap().get("management.port")); + assertEquals("6789", instance.getMetadataMap().get("jmx.port")); + } + + @Test + public void shouldNotResetManagementAndJmxPortsInMetadataMap() throws Exception { + addEnvironment(this.context, "management.port=9999", + "eureka.instance.metadata-map.jmx.port=9898", + "eureka.instance.metadata-map.management.port=7878"); + setupContext(RefreshAutoConfiguration.class); + + EurekaInstanceConfigBean instance = this.context + .getBean(EurekaInstanceConfigBean.class); + assertEquals("7878", instance.getMetadataMap().get("management.port")); + assertEquals("9898", instance.getMetadataMap().get("jmx.port")); + } + @Test public void nonSecurePortPeriods() { testNonSecurePort("server.port"); @@ -153,6 +200,34 @@ public class EurekaClientAutoConfigurationTests { instance.getHealthCheckUrl().contains("/myHealthCheck")); } + @Test + public void statusPageUrl_and_healthCheckUrl_do_not_contain_server_context_path() throws Exception { + addEnvironment(this.context, "server.port=8989", + "management.port=9999", "server.contextPath=/service"); + + setupContext(RefreshAutoConfiguration.class); + EurekaInstanceConfigBean instance = this.context + .getBean(EurekaInstanceConfigBean.class); + assertTrue("Wrong status page: " + instance.getStatusPageUrl(), + instance.getStatusPageUrl().endsWith(":9999/info")); + assertTrue("Wrong health check: " + instance.getHealthCheckUrl(), + instance.getHealthCheckUrl().endsWith(":9999/health")); + } + + @Test + public void statusPageUrl_and_healthCheckUrl_contain_management_context_path() throws Exception { + addEnvironment(this.context, + "server.port=8989", "management.contextPath=/management"); + + setupContext(RefreshAutoConfiguration.class); + EurekaInstanceConfigBean instance = this.context + .getBean(EurekaInstanceConfigBean.class); + assertTrue("Wrong status page: " + instance.getStatusPageUrl(), + instance.getStatusPageUrl().endsWith(":8989/management/info")); + assertTrue("Wrong health check: " + instance.getHealthCheckUrl(), + instance.getHealthCheckUrl().endsWith(":8989/management/health")); + } + @Test public void statusPageUrlPathAndManagementPortAndContextPath() { TestPropertyValues.of( "server.port=8989", diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/metadata/DefaultManagementMetadataProviderTest.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/metadata/DefaultManagementMetadataProviderTest.java new file mode 100644 index 00000000..11b028a5 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/metadata/DefaultManagementMetadataProviderTest.java @@ -0,0 +1,138 @@ +package org.springframework.cloud.netflix.eureka.metadata; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DefaultManagementMetadataProviderTest { + + private static final EurekaInstanceConfigBean INSTANCE = mock(EurekaInstanceConfigBean.class); + private final ManagementMetadataProvider provider = new DefaultManagementMetadataProvider(); + + @Before + public void setUp() throws Exception { + when(INSTANCE.getHostname()).thenReturn("host"); + when(INSTANCE.getHealthCheckUrlPath()).thenReturn("health"); + when(INSTANCE.getStatusPageUrlPath()).thenReturn("info"); + } + + @Test + public void serverPortIsRandomAndManagementPortIsNull() throws Exception { + int serverPort = 0; + String serverContextPath = "/"; + String managementContextPath = null; + Integer managementPort = null; + ManagementMetadata actual = provider.get(INSTANCE, serverPort, serverContextPath, managementContextPath, managementPort); + + assertThat(actual).isNull(); + } + + @Test + public void managementPortIsRandom() throws Exception { + int serverPort = 0; + String serverContextPath = "/"; + String managementContextPath = null; + Integer managementPort = 0; + ManagementMetadata actual = provider.get(INSTANCE, serverPort, serverContextPath, managementContextPath, managementPort); + + assertThat(actual).isNull(); + } + + @Test + public void serverPort() throws Exception { + int serverPort = 7777; + String serverContextPath = "/"; + String managementContextPath = null; + Integer managementPort = null; + ManagementMetadata actual = provider.get(INSTANCE, serverPort, serverContextPath, managementContextPath, managementPort); + + assertThat(actual.getHealthCheckUrl()).isEqualTo("http://host:7777/health"); + assertThat(actual.getStatusPageUrl()).isEqualTo("http://host:7777/info"); + assertThat(actual.getManagementPort()).isEqualTo(7777); + } + + @Test + public void serverPortManagementPort() throws Exception { + int serverPort = 7777; + String serverContextPath = "/"; + String managementContextPath = null; + Integer managementPort = 8888; + ManagementMetadata actual = provider.get(INSTANCE, serverPort, serverContextPath, managementContextPath, managementPort); + + assertThat(actual.getHealthCheckUrl()).isEqualTo("http://host:8888/health"); + assertThat(actual.getStatusPageUrl()).isEqualTo("http://host:8888/info"); + assertThat(actual.getManagementPort()).isEqualTo(8888); + } + + @Test + public void serverPortManagementPortServerContextPath() throws Exception { + int serverPort = 7777; + String serverContextPath = "/Server"; + String managementContextPath = null; + Integer managementPort = 8888; + ManagementMetadata actual = provider.get(INSTANCE, serverPort, serverContextPath, managementContextPath, managementPort); + + assertThat(actual.getHealthCheckUrl()).isEqualTo("http://host:8888/health"); + assertThat(actual.getStatusPageUrl()).isEqualTo("http://host:8888/info"); + assertThat(actual.getManagementPort()).isEqualTo(8888); + } + + @Test + public void serverPortManagementPortServerContextPathManagementContextPath() throws Exception { + int serverPort = 7777; + String serverContextPath = "/Server"; + String managementContextPath = "/Management"; + Integer managementPort = 8888; + ManagementMetadata actual = provider.get(INSTANCE, serverPort, serverContextPath, managementContextPath, managementPort); + + assertThat(actual.getHealthCheckUrl()).isEqualTo("http://host:8888/Management/health"); + assertThat(actual.getStatusPageUrl()).isEqualTo("http://host:8888/Management/info"); + assertThat(actual.getManagementPort()).isEqualTo(8888); + } + + @Test + public void serverPortServerContextPathManagementContextPath() throws Exception { + int serverPort = 7777; + String serverContextPath = "/Server"; + String managementContextPath = "/Management"; + Integer managementPort = null; + ManagementMetadata actual = provider.get(INSTANCE, serverPort, serverContextPath, managementContextPath, managementPort); + + assertThat(actual.getHealthCheckUrl()).isEqualTo("http://host:7777/Management/health"); + assertThat(actual.getStatusPageUrl()).isEqualTo("http://host:7777/Management/info"); + assertThat(actual.getManagementPort()).isEqualTo(7777); + } + + @Test + public void serverPortServerContextPath() throws Exception { + int serverPort = 7777; + String serverContextPath = "/Server"; + String managementContextPath = null; + Integer managementPort = null; + ManagementMetadata actual = provider.get(INSTANCE, serverPort, serverContextPath, managementContextPath, managementPort); + + assertThat(actual.getHealthCheckUrl()).isEqualTo("http://host:7777/Server/health"); + assertThat(actual.getStatusPageUrl()).isEqualTo("http://host:7777/Server/info"); + assertThat(actual.getManagementPort()).isEqualTo(7777); + } + + @Test + public void serverPortManagementPortManagementContextPath() throws Exception { + int serverPort = 7777; + String serverContextPath = "/"; + String managementContextPath = "/Management"; + Integer managementPort = 8888; + ManagementMetadata actual = provider.get(INSTANCE, serverPort, serverContextPath, managementContextPath, managementPort); + + assertThat(actual.getHealthCheckUrl()).isEqualTo("http://host:8888/Management/health"); + assertThat(actual.getStatusPageUrl()).isEqualTo("http://host:8888/Management/info"); + assertThat(actual.getManagementPort()).isEqualTo(8888); + + } + + +} \ No newline at end of file diff --git a/spring-cloud-netflix-hystrix-amqp/pom.xml b/spring-cloud-netflix-hystrix-amqp/pom.xml new file mode 100644 index 00000000..e69de29b diff --git a/spring-cloud-netflix-spectator/pom.xml b/spring-cloud-netflix-spectator/pom.xml new file mode 100644 index 00000000..e69de29b diff --git a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyMarkerConfiguration.java b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyMarkerConfiguration.java index 0e88776c..451442bf 100644 --- a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyMarkerConfiguration.java +++ b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyMarkerConfiguration.java @@ -22,7 +22,7 @@ import org.springframework.context.annotation.Configuration; /** * Responsible for adding in a marker bean to trigger activation of - * {@link ZuulServerAutoConfiguration} + * {@link ZuulProxyAutoConfiguration} * * @author Biju Kunjummen */ diff --git a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java index 771ea7b9..658882ae 100644 --- a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java +++ b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java @@ -44,6 +44,7 @@ import static org.springframework.cloud.netflix.zuul.filters.support.FilterConst import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.RIBBON_ROUTING_FILTER_ORDER; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ROUTE_TYPE; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.LOAD_BALANCER_KEY; /** * Route {@link ZuulFilter} that uses Ribbon, Hystrix and pluggable http clients to send requests. @@ -135,6 +136,7 @@ public class RibbonRoutingFilter extends ZuulFilter { String serviceId = (String) context.get(SERVICE_ID_KEY); Boolean retryable = (Boolean) context.get(RETRYABLE_KEY); + Object loadBalancerKey = context.get(LOAD_BALANCER_KEY); String uri = this.helper.buildZuulRequestURI(request); @@ -144,7 +146,7 @@ public class RibbonRoutingFilter extends ZuulFilter { long contentLength = useServlet31 ? request.getContentLengthLong(): request.getContentLength(); return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params, - requestEntity, this.requestCustomizers, contentLength); + requestEntity, this.requestCustomizers, contentLength, loadBalancerKey); } protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception { diff --git a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/support/FilterConstants.java b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/support/FilterConstants.java index bf5f9c01..a2c24b3a 100644 --- a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/support/FilterConstants.java +++ b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/support/FilterConstants.java @@ -70,6 +70,11 @@ public class FilterConstants { */ public static final String SERVICE_ID_KEY = "serviceId"; + /** + * Zuul {@link com.netflix.zuul.context.RequestContext} key for use in {@link org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter} + */ + public static final String LOAD_BALANCER_KEY = "loadBalancerKey"; + // ORDER constants ----------------------------------- /** diff --git a/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilterLoadBalancerKeyIntegrationTests.java b/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilterLoadBalancerKeyIntegrationTests.java new file mode 100644 index 00000000..6acd0506 --- /dev/null +++ b/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilterLoadBalancerKeyIntegrationTests.java @@ -0,0 +1,178 @@ +/* + * 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.filters.route; + +import com.netflix.loadbalancer.AvailabilityFilteringRule; +import com.netflix.loadbalancer.IRule; +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.ServerList; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import org.junit.After; +import org.junit.Before; +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.boot.web.server.LocalServerPort; +import org.springframework.cloud.netflix.ribbon.RibbonClient; +import org.springframework.cloud.netflix.ribbon.StaticServerList; +import org.springframework.cloud.netflix.zuul.EnableZuulProxy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.*; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +import static org.junit.Assert.assertEquals; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.LOAD_BALANCER_KEY; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; + +/** + * @author Yongsung Yoon + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = CanaryTestZuulProxyApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + value = { "zuul.routes.simple.path: /simple/**" }) +@DirtiesContext +public class RibbonRoutingFilterLoadBalancerKeyIntegrationTests { + + @Autowired + private TestRestTemplate testRestTemplate; + + @Before + public void setTestRequestContext() { + RequestContext context = new RequestContext(); + RequestContext.testSetCurrentContext(context); + } + + @After + public void clear() { + RequestContext.getCurrentContext().clear(); + } + + @Test + public void invokeWithUserDefinedCanaryHeader() { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Canary-Test", "true"); + + ResponseEntity result = testRestTemplate.exchange("/simple/hello", HttpMethod.GET, + new HttpEntity<>(headers), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("canary", result.getBody()); + } + + @Test + public void invokeWithoutUserDefinedCanaryHeader() { + HttpHeaders headers = new HttpHeaders(); + ResponseEntity result = testRestTemplate.exchange("/simple/hello", HttpMethod.GET, + new HttpEntity<>(headers), String.class); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatusCode()); + } +} + +@Configuration +@EnableAutoConfiguration +@RestController +@EnableZuulProxy +@RibbonClient(name = "simple", configuration = CanaryTestRibbonClientConfiguration.class) +class CanaryTestZuulProxyApplication { + + @RequestMapping(value = "/hello", method = RequestMethod.GET) + public String hello() { + return "canary"; + } + + @Bean + public ZuulFilter testCanarySupportPreFilter() { + return new ZuulFilter() { + @Override + public Object run() { + RequestContext context = RequestContext.getCurrentContext(); + if (checkIfCanaryRequest(context)) { + context.set(LOAD_BALANCER_KEY, "canary"); // set loadBalancerKey for IRule + } + return null; + } + + private boolean checkIfCanaryRequest(RequestContext context) { + HttpServletRequest request = context.getRequest(); + String canaryHeader = request.getHeader("X-Canary-Test"); // user defined header + + if ((canaryHeader != null) && (canaryHeader.equalsIgnoreCase("true"))) { + return true; + } + return false; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public String filterType() { + return PRE_TYPE; + } + + @Override + public int filterOrder() { + return 0; + } + }; + } +} + +@Configuration +class CanaryTestRibbonClientConfiguration { + + @LocalServerPort + private int port; + + private static Server testCanaryInstance; + + @Bean + public ServerList ribbonServerList() { + return new StaticServerList<>(new Server("normal-routing-notexist-localhost", this.port)); + } + + @Bean + public IRule canaryTestRule() { + if (testCanaryInstance == null) { + testCanaryInstance = new Server("localhost", port); // use test server as a canary instance + } + return new TestCanaryRule(); + } + + public static class TestCanaryRule extends AvailabilityFilteringRule { + @Override + public Server choose(Object key) { + if ((key != null) && (key.equals("canary"))) { + return testCanaryInstance; // choose test canary server instead of normal servers. + } + return super.choose(key); // normal routing + } + } +} diff --git a/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilterTests.java b/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilterTests.java index 909c6fab..7366e848 100644 --- a/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilterTests.java +++ b/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilterTests.java @@ -19,22 +19,74 @@ package org.springframework.cloud.netflix.zuul.filters.route; import java.util.Collections; +import com.netflix.zuul.context.RequestContext; +import org.junit.After; +import org.junit.Before; import org.junit.Test; +import org.springframework.cloud.netflix.ribbon.support.RibbonCommandContext; import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer; import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; +import org.springframework.mock.web.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.LOAD_BALANCER_KEY; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; /** * @author Spencer Gibb + * @author Yongsung Yoon */ public class RibbonRoutingFilterTests { + + private RequestContext requestContext; + private RibbonRoutingFilter filter; + + @Before + public void setUp() throws Exception { + setUpRequestContext(); + setupRibbonRoutingFilter(); + } + + @After + public void tearDown() throws Exception { + requestContext.unset(); + } + @Test public void useServlet31Works() { - RibbonCommandFactory factory = mock(RibbonCommandFactory.class); - RibbonRoutingFilter filter = new RibbonRoutingFilter(new ProxyRequestHelper(), factory, - Collections.emptyList()); assertThat(filter.isUseServlet31()).isTrue(); } + + @Test + public void testLoadBalancerKeyToRibbonCommandContext() throws Exception { + final String testKey = "testLoadBalancerKey"; + requestContext.set(LOAD_BALANCER_KEY, testKey); + RibbonCommandContext commandContext = filter.buildCommandContext(requestContext); + + assertThat(commandContext.getLoadBalancerKey()).isEqualTo(testKey); + } + + @Test + public void testNullLoadBalancerKeyToRibbonCommandContext() throws Exception { + requestContext.set(LOAD_BALANCER_KEY, null); + RibbonCommandContext commandContext = filter.buildCommandContext(requestContext); + + assertThat(commandContext.getLoadBalancerKey()).isNull(); + } + + private void setUpRequestContext() { + requestContext = RequestContext.getCurrentContext(); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.setMethod("GET"); + mockRequest.setRequestURI("/foo/bar"); + requestContext.setRequest(mockRequest); + requestContext.setRequestQueryParams(Collections.EMPTY_MAP); + requestContext.set(SERVICE_ID_KEY, "testServiceId"); + } + + private void setupRibbonRoutingFilter() { + RibbonCommandFactory factory = mock(RibbonCommandFactory.class); + filter = new RibbonRoutingFilter(new ProxyRequestHelper(), factory, Collections.emptyList()); + } }