diff --git a/.gitignore b/.gitignore index 67907698..81428507 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ _site/ .factorypath *.log .shelf +*.swp +*.swo diff --git a/docs/pom.xml b/docs/pom.xml index 201718f4..ddf45464 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -14,6 +14,7 @@ spring-cloud-netflix ${basedir}/.. + 1.0.x,1.1.x diff --git a/docs/src/main/asciidoc/ghpages.sh b/docs/src/main/asciidoc/ghpages.sh index e1063ce3..a5d1acd5 100755 --- a/docs/src/main/asciidoc/ghpages.sh +++ b/docs/src/main/asciidoc/ghpages.sh @@ -12,43 +12,118 @@ if ! [ -d docs/target/generated-docs ]; then exit 0 fi -# Find name of current branch +# The script should be executed from the root folder + +ROOT_FOLDER=`pwd` +echo "Current folder is ${ROOT_FOLDER}" + +if [[ ! -e "${ROOT_FOLDER}/.git" ]]; then + echo "You're not in the root folder of the project!" + exit 1 +fi + +# Retrieve properties ################################################################### -branch=$TRAVIS_BRANCH -[ "$branch" == "" ] && branch=`git rev-parse --abbrev-ref HEAD` -target=. -if [ "$branch" != "master" ]; then target=./$branch; mkdir -p $target; fi + +# Prop that will let commit the changes +COMMIT_CHANGES="no" +MAVEN_PATH=${MAVEN_PATH:-} +echo "Path to Maven is [${MAVEN_PATH}]" + +# Code getting the name of the current branch. For master we want to publish as we did until now +# http://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch +# If there is a branch already passed will reuse it - otherwise will try to find it +CURRENT_BRANCH=${BRANCH} +if [[ -z "${CURRENT_BRANCH}" ]] ; then + CURRENT_BRANCH=$(git symbolic-ref -q HEAD) + CURRENT_BRANCH=${CURRENT_BRANCH##refs/heads/} + CURRENT_BRANCH=${CURRENT_BRANCH:-HEAD} +fi +echo "Current branch is [${CURRENT_BRANCH}]" +git checkout ${CURRENT_BRANCH} + +# Get the name of the `docs.main` property +MAIN_ADOC_VALUE=$("${MAVEN_PATH}"mvn -q \ + -Dexec.executable="echo" \ + -Dexec.args='${docs.main}' \ + --non-recursive \ + org.codehaus.mojo:exec-maven-plugin:1.3.1:exec) +echo "Extracted 'main.adoc' from Maven build [${MAIN_ADOC_VALUE}]" + +# Get whitelisted branches - assumes that a `docs` module is available under `docs` profile +WHITELIST_PROPERTY="docs.whitelisted.branches" +WHITELISTED_BRANCHES_VALUE=$("${MAVEN_PATH}"mvn -q \ + -Dexec.executable="echo" \ + -Dexec.args="\${${WHITELIST_PROPERTY}}" \ + org.codehaus.mojo:exec-maven-plugin:1.3.1:exec \ + -P docs \ + -pl docs) +echo "Extracted '${WHITELIST_PROPERTY}' from Maven build [${WHITELISTED_BRANCHES_VALUE}]" # Stash any outstanding changes ################################################################### -git diff-index --quiet HEAD -dirty=$? +git diff-index --quiet HEAD && dirty=$? || (echo "Failed to check if the current repo is dirty. Assuming that it is." && dirty="1") if [ "$dirty" != "0" ]; then git stash; fi -# Switch to gh-pages branch to sync it with current branch +# Switch to gh-pages branch to sync it with master ################################################################### git checkout gh-pages +git pull origin gh-pages -for f in docs/target/generated-docs/*; do - file=${f#docs/target/generated-docs/*} - if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then - # Not ignored... - cp -rf $f $target - git add -A $target/$file - fi -done - -git add -A README.adoc || echo "No change to README.adoc" -git commit -a -m "Sync docs from $branch to gh-pages" || echo "Nothing committed" - -# Uncomment the following push if you want to auto push to -# the gh-pages branch whenever you commit to branch locally. -# This is a little extreme. Use with care! +# Add git branches ################################################################### -git push origin gh-pages || echo "Cannot push gh-pages" +mkdir -p ${ROOT_FOLDER}/${CURRENT_BRANCH} +if [[ "${CURRENT_BRANCH}" == "master" ]] ; then + echo -e "Current branch is master - will copy the current docs only to the root folder" + for f in docs/target/generated-docs/*; do + file=${f#docs/target/generated-docs/*} + if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then + # Not ignored... + cp -rf $f ${ROOT_FOLDER}/ + git add -A ${ROOT_FOLDER}/$file + fi + done + COMMIT_CHANGES="yes" +else + echo -e "Current branch is [${CURRENT_BRANCH}]" + # http://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin + if [[ ",${WHITELISTED_BRANCHES_VALUE}," = *",${CURRENT_BRANCH},"* ]] ; then + echo -e "Branch [${CURRENT_BRANCH}] is whitelisted! Will copy the current docs to the [${CURRENT_BRANCH}] folder" + for f in docs/target/generated-docs/*; do + file=${f#docs/target/generated-docs/*} + if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then + # Not ignored... + # We want users to access 1.0.0.RELEASE/ instead of 1.0.0.RELEASE/spring-cloud.sleuth.html + if [[ "${file}" == "${MAIN_ADOC_VALUE}.html" ]] ; then + # We don't want to copy the spring-cloud-sleuth.html + # we want it to be converted to index.html + cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html + git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html + else + cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH} + git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/$file + fi + fi + done + COMMIT_CHANGES="yes" + else + echo -e "Branch [${CURRENT_BRANCH}] is not on the white list! Check out the Maven [${WHITELIST_PROPERTY}] property in + [docs] module available under [docs] profile. Won't commit any changes to gh-pages for this branch." + fi +fi -# Finally, switch back to the current branch and exit block -git checkout $branch +if [[ "${COMMIT_CHANGES}" == "yes" ]] ; then + git commit -a -m "Sync docs from ${CURRENT_BRANCH} to gh-pages" + + # Uncomment the following push if you want to auto push to + # the gh-pages branch whenever you commit to master locally. + # This is a little extreme. Use with care! + ################################################################### + git push origin gh-pages +fi + +# Finally, switch back to the master branch and exit block +git checkout ${CURRENT_BRANCH} if [ "$dirty" != "0" ]; then git stash pop; fi -exit 0 +exit 0 \ No newline at end of file diff --git a/scripts/runAcceptanceTests.sh b/scripts/runAcceptanceTests.sh index 0fc9429a..97a9d6bf 100755 --- a/scripts/runAcceptanceTests.sh +++ b/scripts/runAcceptanceTests.sh @@ -14,9 +14,9 @@ curl "${SCRIPT_URL}" --output runAcceptanceTests.sh chmod +x runAcceptanceTests.sh echo "Killing all running apps" -./runAcceptanceTests.sh -t "${AT_WHAT_TO_TEST}" -n +./runAcceptanceTests.sh -t "${AT_WHAT_TO_TEST}" --killnow -./runAcceptanceTests.sh -t "${AT_WHAT_TO_TEST}" -k +./runAcceptanceTests.sh -t "${AT_WHAT_TO_TEST}" --killattheend SCRIPT_URL="https://raw.githubusercontent.com/spring-cloud-samples/tests/master/scripts/runTests.sh" diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java index 3bdb633b..9d3278df 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java @@ -82,6 +82,11 @@ public class ZuulProperties { */ private boolean addProxyHeaders = true; + /** + * Flag to determine whether the proxy forwards the Host header. + */ + private boolean addHostHeader = false; + /** * Set of service names not to consider for proxying automatically. By default all * services in the discovery client will be proxied. diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java index 39dc918b..6db078a9 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java @@ -19,6 +19,8 @@ package org.springframework.cloud.netflix.zuul.filters.pre; import java.net.MalformedURLException; import java.net.URL; +import javax.servlet.http.HttpServletRequest; + import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; import org.springframework.cloud.netflix.zuul.filters.Route; import org.springframework.cloud.netflix.zuul.filters.RouteLocator; @@ -36,6 +38,8 @@ import lombok.extern.apachecommons.CommonsLog; @CommonsLog public class PreDecorationFilter extends ZuulFilter { + public static final int FILTER_ORDER = 5; + private RouteLocator routeLocator; private String dispatcherServletPath; @@ -58,7 +62,7 @@ public class PreDecorationFilter extends ZuulFilter { @Override public int filterOrder() { - return 5; + return FILTER_ORDER; } @Override @@ -115,29 +119,34 @@ public class PreDecorationFilter extends ZuulFilter { ctx.addOriginResponseHeader("X-Zuul-ServiceId", location); } if (this.properties.isAddProxyHeaders()) { - ctx.addZuulRequestHeader("X-Forwarded-Host", - ctx.getRequest().getServerName()); + ctx.addZuulRequestHeader("X-Forwarded-Host", toHostHeader(ctx.getRequest())); ctx.addZuulRequestHeader("X-Forwarded-Port", String.valueOf(ctx.getRequest().getServerPort())); ctx.addZuulRequestHeader(ZuulHeaders.X_FORWARDED_PROTO, ctx.getRequest().getScheme()); + String forwardedPrefix = + ctx.getRequest().getHeader("X-Forwarded-Prefix"); + String contextPath = ctx.getRequest().getContextPath(); + String prefix = StringUtils.hasLength(forwardedPrefix) + ? forwardedPrefix + : (StringUtils.hasLength(contextPath) ? contextPath : null); if (StringUtils.hasText(route.getPrefix())) { - String existingPrefix = ctx.getRequest() - .getHeader("X-Forwarded-Prefix"); StringBuilder newPrefixBuilder = new StringBuilder(); - if (StringUtils.hasLength(existingPrefix)) { - if (existingPrefix.endsWith("/") + if (prefix != null) { + if (prefix.endsWith("/") && route.getPrefix().startsWith("/")) { - newPrefixBuilder.append(existingPrefix, 0, - existingPrefix.length() - 1); + newPrefixBuilder.append(prefix, 0, + prefix.length() - 1); } else { - newPrefixBuilder.append(existingPrefix); + newPrefixBuilder.append(prefix); } } newPrefixBuilder.append(route.getPrefix()); - ctx.addZuulRequestHeader("X-Forwarded-Prefix", - newPrefixBuilder.toString()); + prefix = newPrefixBuilder.toString(); + } + if (prefix != null) { + ctx.addZuulRequestHeader("X-Forwarded-Prefix", prefix); } String xforwardedfor = ctx.getRequest().getHeader("X-Forwarded-For"); String remoteAddr = ctx.getRequest().getRemoteAddr(); @@ -149,6 +158,9 @@ public class PreDecorationFilter extends ZuulFilter { } ctx.addZuulRequestHeader("X-Forwarded-For", xforwardedfor); } + if (this.properties.isAddHostHeader()) { + ctx.addZuulRequestHeader("Host", toHostHeader(ctx.getRequest())); + } } } else { @@ -182,6 +194,15 @@ public class PreDecorationFilter extends ZuulFilter { return null; } + private String toHostHeader(HttpServletRequest request) { + int port = request.getServerPort(); + if ((port == 80 && "http".equals(request.getScheme())) || (port == 443 && "https".equals(request.getScheme()))) { + return request.getServerName(); + } else { + return request.getServerName() + ":" + port; + } + } + private URL getUrl(String target) { try { return new URL(target); diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/resttemplate/RestTemplateRetryTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/resttemplate/RestTemplateRetryTests.java similarity index 93% rename from spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/resttemplate/RestTemplateRetryTest.java rename to spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/resttemplate/RestTemplateRetryTests.java index 8166bfb2..c9c0285c 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/resttemplate/RestTemplateRetryTest.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/resttemplate/RestTemplateRetryTests.java @@ -1,290 +1,289 @@ -package org.springframework.cloud.netflix.resttemplate; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.net.UnknownHostException; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicInteger; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.cloud.client.loadbalancer.LoadBalanced; -import org.springframework.cloud.netflix.ribbon.RibbonClient; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.RestTemplate; - -import com.netflix.client.RetryHandler; -import com.netflix.client.config.IClientConfig; -import com.netflix.loadbalancer.AvailabilityFilteringRule; -import com.netflix.loadbalancer.BaseLoadBalancer; -import com.netflix.loadbalancer.ILoadBalancer; -import com.netflix.loadbalancer.IPing; -import com.netflix.loadbalancer.IRule; -import com.netflix.loadbalancer.LoadBalancerBuilder; -import com.netflix.loadbalancer.LoadBalancerStats; -import com.netflix.loadbalancer.Server; -import com.netflix.loadbalancer.ServerList; -import com.netflix.loadbalancer.ServerStats; -import com.netflix.niws.client.http.HttpClientLoadBalancerErrorHandler; - -@RunWith(SpringJUnit4ClassRunner.class) -@SpringBootTest(classes = RestTemplateRetryTest.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT, value = { - "spring.application.name=resttemplatetest", - "logging.level.org.springframework.cloud.netflix.resttemplate=DEBUG", - "badClients.ribbon.MaxAutoRetries=0", - "badClients.ribbon.OkToRetryOnAllOperations=true", "ribbon.http.client.enabled" }) -@DirtiesContext -public class RestTemplateRetryTest { - - final private static Log logger = LogFactory.getLog(RestTemplateRetryTest.class); - - @Value("${local.server.port}") - private int port = 0; - - @Autowired - private RestTemplate testClient; - - public RestTemplateRetryTest() { - } - - @Configuration - @EnableAutoConfiguration - @RestController - @RibbonClient(name = "badClients", configuration = LocalBadClientConfiguration.class) - public static class Application { - - private AtomicInteger hits = new AtomicInteger(1); - private AtomicInteger retryHits = new AtomicInteger(1); - - @RequestMapping(method = RequestMethod.GET, value = "/ping") - public int ping() { - return 0; - } - - @RequestMapping(method = RequestMethod.GET, value = "/good") - public int good() { - int lValue = this.hits.getAndIncrement(); - return lValue; - } - - @RequestMapping(method = RequestMethod.GET, value = "/timeout") - public int timeout() throws Exception { - int lValue = this.retryHits.getAndIncrement(); - - // Force the good server to have 2 consecutive errors a couple of times. - if (lValue == 2 || lValue == 3 || lValue == 5 || lValue == 6) { - Thread.sleep(500); - } - return lValue; - } - - @RequestMapping(method = RequestMethod.GET, value = "/null") - public int isNull() throws Exception { - throw new NullPointerException("Null"); - } - - @LoadBalanced - @Bean - RestTemplate restTemplate() { - return new RestTemplate(); - } - } - - @Before - public void setup() throws Exception { - // Force Ribbon configuration by making one call. - this.testClient.getForObject("http://badClients/ping", Integer.class); - } - - @Test - public void testNullPointer() throws Exception { - - LoadBalancerStats stats = LocalBadClientConfiguration.balancer - .getLoadBalancerStats(); - ServerStats badServer1Stats = stats - .getSingleServerStat(LocalBadClientConfiguration.badServer); - ServerStats badServer2Stats = stats - .getSingleServerStat(LocalBadClientConfiguration.badServer2); - ServerStats goodServerStats = stats - .getSingleServerStat(LocalBadClientConfiguration.goodServer); - - badServer1Stats.clearSuccessiveConnectionFailureCount(); - badServer2Stats.clearSuccessiveConnectionFailureCount(); - long targetConnectionCount = goodServerStats.getTotalRequestsCount() + 10; - - // A null pointer should NOT trigger a circuit breaker. - for (int index = 0; index < 10; index++) { - try { - this.testClient.getForObject("http://badClients/null", Integer.class); - } - catch (Exception exception) { - } - } - logServerStats(LocalBadClientConfiguration.badServer); - logServerStats(LocalBadClientConfiguration.badServer2); - logServerStats(LocalBadClientConfiguration.goodServer); - - assertTrue(badServer1Stats.isCircuitBreakerTripped()); - assertTrue(badServer2Stats.isCircuitBreakerTripped()); - assertEquals(targetConnectionCount, goodServerStats.getTotalRequestsCount()); - - // Wait for any timeout thread to finish. - - } - - private void logServerStats(Server server) { - LoadBalancerStats stats = LocalBadClientConfiguration.balancer - .getLoadBalancerStats(); - ServerStats serverStats = stats.getSingleServerStat(server); - logger.debug("Server : " + server.toString() + " : Total Count == " - + serverStats.getTotalRequestsCount() + ", Failure Count == " - + serverStats.getFailureCount() + ", Successive Connection Failure == " - + serverStats.getSuccessiveConnectionFailureCount() - + ", Circuit Breaker ? == " + serverStats.isCircuitBreakerTripped()); - } - - @Test - public void testRestRetries() { - - LoadBalancerStats stats = LocalBadClientConfiguration.balancer - .getLoadBalancerStats(); - ServerStats badServer1Stats = stats - .getSingleServerStat(LocalBadClientConfiguration.badServer); - ServerStats badServer2Stats = stats - .getSingleServerStat(LocalBadClientConfiguration.badServer2); - ServerStats goodServerStats = stats - .getSingleServerStat(LocalBadClientConfiguration.goodServer); - - badServer1Stats.clearSuccessiveConnectionFailureCount(); - badServer2Stats.clearSuccessiveConnectionFailureCount(); - long targetConnectionCount = goodServerStats.getTotalRequestsCount() + 20; - - int hits = 0; - - for (int index = 0; index < 20; index++) { - hits = this.testClient.getForObject("http://badClients/good", Integer.class); - } - - logServerStats(LocalBadClientConfiguration.badServer); - logServerStats(LocalBadClientConfiguration.badServer2); - logServerStats(LocalBadClientConfiguration.goodServer); - - assertTrue(badServer1Stats.isCircuitBreakerTripped()); - assertTrue(badServer2Stats.isCircuitBreakerTripped()); - assertEquals(targetConnectionCount, goodServerStats.getTotalRequestsCount()); - assertEquals(20, hits); - System.out.println("Retry Hits: " + hits); - } - - @Test - public void testRestRetriesWithReadTimeout() throws Exception { - - LoadBalancerStats stats = LocalBadClientConfiguration.balancer - .getLoadBalancerStats(); - ServerStats badServer1Stats = stats - .getSingleServerStat(LocalBadClientConfiguration.badServer); - ServerStats badServer2Stats = stats - .getSingleServerStat(LocalBadClientConfiguration.badServer2); - ServerStats goodServerStats = stats - .getSingleServerStat(LocalBadClientConfiguration.goodServer); - - badServer1Stats.clearSuccessiveConnectionFailureCount(); - badServer2Stats.clearSuccessiveConnectionFailureCount(); - assertTrue(!badServer1Stats.isCircuitBreakerTripped()); - assertTrue(!badServer2Stats.isCircuitBreakerTripped()); - - int hits = 0; - - for (int index = 0; index < 15; index++) { - hits = this.testClient.getForObject("http://badClients/timeout", - Integer.class); - } - logServerStats(LocalBadClientConfiguration.badServer); - logServerStats(LocalBadClientConfiguration.badServer2); - logServerStats(LocalBadClientConfiguration.goodServer); - - assertTrue(badServer1Stats.isCircuitBreakerTripped()); - assertTrue(badServer2Stats.isCircuitBreakerTripped()); - assertTrue(!goodServerStats.isCircuitBreakerTripped()); - - // 15 + 4 timeouts. See the endpoint for timeout conditions. - assertEquals(19, hits); - - // Wait for any timeout thread to finish. - Thread.sleep(600); - - } - -} - -// Load balancer with fixed server list for "local" pointing to localhost -// and some bogus servers are thrown in to test retry -@Configuration -class LocalBadClientConfiguration { - - static BaseLoadBalancer balancer; - static Server goodServer; - static Server badServer; - static Server badServer2; - - public LocalBadClientConfiguration() { - } - - @Value("${local.server.port}") - private int port = 0; - - @Bean - public IRule loadBalancerRule() { - // This is a good place to try different load balancing rules and how those rules - // behave in failure - // states: BestAvailableRule, WeightedResponseTimeRule, etc - - // This rule just uses a round robin and will skip servers that are in circuit - // breaker state. - return new AvailabilityFilteringRule(); - - } - - @Bean - public ILoadBalancer ribbonLoadBalancer(IClientConfig config, - ServerList serverList, IRule rule, IPing ping) { - - goodServer = new Server("localhost", this.port); - badServer = new Server("mybadhost", 10001); - badServer2 = new Server("localhost", -1); - - balancer = LoadBalancerBuilder.newBuilder().withClientConfig(config) - .withRule(rule).withPing(ping).buildFixedServerListLoadBalancer( - Arrays.asList(badServer, badServer2, goodServer)); - return balancer; - } - - @Bean - public RetryHandler retryHandler() { - return new OverrideRetryHandler(); - } - - static class OverrideRetryHandler extends HttpClientLoadBalancerErrorHandler { - public OverrideRetryHandler() { - this.circuitRelated.add(UnknownHostException.class); - this.retriable.add(UnknownHostException.class); - - } - } - -} +package org.springframework.cloud.netflix.resttemplate; + +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.netflix.ribbon.RibbonClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.SocketUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import com.netflix.client.RetryHandler; +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.AvailabilityFilteringRule; +import com.netflix.loadbalancer.BaseLoadBalancer; +import com.netflix.loadbalancer.ILoadBalancer; +import com.netflix.loadbalancer.IPing; +import com.netflix.loadbalancer.IRule; +import com.netflix.loadbalancer.LoadBalancerBuilder; +import com.netflix.loadbalancer.LoadBalancerStats; +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.ServerList; +import com.netflix.loadbalancer.ServerStats; +import com.netflix.niws.client.http.HttpClientLoadBalancerErrorHandler; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = RestTemplateRetryTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT, value = { + "spring.application.name=resttemplatetest", + "logging.level.org.springframework.cloud.netflix.resttemplate=DEBUG", + "logging.level.com.netflix=DEBUG", "badClients.ribbon.MaxAutoRetries=0", + "badClients.ribbon.OkToRetryOnAllOperations=true", "ribbon.http.client.enabled" }) +@DirtiesContext +public class RestTemplateRetryTests { + + final private static Log logger = LogFactory.getLog(RestTemplateRetryTests.class); + + @Value("${local.server.port}") + private int port = 0; + + @Autowired + private RestTemplate testClient; + + public RestTemplateRetryTests() { + } + + @Before + public void setup() throws Exception { + // Force Ribbon configuration by making one call. + this.testClient.getForObject("http://badClients/ping", Integer.class); + } + + @Test + public void testNullPointer() throws Exception { + + LoadBalancerStats stats = LocalBadClientConfiguration.balancer + .getLoadBalancerStats(); + ServerStats badServer1Stats = stats + .getSingleServerStat(LocalBadClientConfiguration.badServer); + ServerStats badServer2Stats = stats + .getSingleServerStat(LocalBadClientConfiguration.badServer2); + ServerStats goodServerStats = stats + .getSingleServerStat(LocalBadClientConfiguration.goodServer); + + badServer1Stats.clearSuccessiveConnectionFailureCount(); + badServer2Stats.clearSuccessiveConnectionFailureCount(); + long targetConnectionCount = goodServerStats.getTotalRequestsCount() + 10; + + // A null pointer should NOT trigger a circuit breaker. + for (int index = 0; index < 10; index++) { + try { + this.testClient.getForObject("http://badClients/null", Integer.class); + } + catch (Exception exception) { + } + } + logServerStats(LocalBadClientConfiguration.badServer); + logServerStats(LocalBadClientConfiguration.badServer2); + logServerStats(LocalBadClientConfiguration.goodServer); + + assertTrue(badServer1Stats.isCircuitBreakerTripped()); + assertTrue(badServer2Stats.isCircuitBreakerTripped()); + assertEquals(targetConnectionCount, goodServerStats.getTotalRequestsCount()); + + // Wait for any timeout thread to finish. + + } + + private void logServerStats(Server server) { + LoadBalancerStats stats = LocalBadClientConfiguration.balancer + .getLoadBalancerStats(); + ServerStats serverStats = stats.getSingleServerStat(server); + logger.debug("Server : " + server.toString() + " : Total Count == " + + serverStats.getTotalRequestsCount() + ", Failure Count == " + + serverStats.getFailureCount() + ", Successive Connection Failure == " + + serverStats.getSuccessiveConnectionFailureCount() + + ", Circuit Breaker ? == " + serverStats.isCircuitBreakerTripped()); + } + + @Test + public void testRestRetries() { + + LoadBalancerStats stats = LocalBadClientConfiguration.balancer + .getLoadBalancerStats(); + ServerStats badServer1Stats = stats + .getSingleServerStat(LocalBadClientConfiguration.badServer); + ServerStats badServer2Stats = stats + .getSingleServerStat(LocalBadClientConfiguration.badServer2); + ServerStats goodServerStats = stats + .getSingleServerStat(LocalBadClientConfiguration.goodServer); + + badServer1Stats.clearSuccessiveConnectionFailureCount(); + badServer2Stats.clearSuccessiveConnectionFailureCount(); + long targetConnectionCount = goodServerStats.getTotalRequestsCount() + 20; + + int hits = 0; + + for (int index = 0; index < 20; index++) { + hits = this.testClient.getForObject("http://badClients/good", Integer.class); + } + + logServerStats(LocalBadClientConfiguration.badServer); + logServerStats(LocalBadClientConfiguration.badServer2); + logServerStats(LocalBadClientConfiguration.goodServer); + + assertTrue(badServer1Stats.isCircuitBreakerTripped()); + assertTrue(badServer2Stats.isCircuitBreakerTripped()); + assertEquals(targetConnectionCount, goodServerStats.getTotalRequestsCount()); + assertEquals(20, hits); + logger.debug("Retry Hits: " + hits); + } + + @Test + public void testRestRetriesWithReadTimeout() throws Exception { + + LoadBalancerStats stats = LocalBadClientConfiguration.balancer + .getLoadBalancerStats(); + ServerStats badServer1Stats = stats + .getSingleServerStat(LocalBadClientConfiguration.badServer); + ServerStats badServer2Stats = stats + .getSingleServerStat(LocalBadClientConfiguration.badServer2); + ServerStats goodServerStats = stats + .getSingleServerStat(LocalBadClientConfiguration.goodServer); + + badServer1Stats.clearSuccessiveConnectionFailureCount(); + badServer2Stats.clearSuccessiveConnectionFailureCount(); + assertTrue(!badServer1Stats.isCircuitBreakerTripped()); + assertTrue(!badServer2Stats.isCircuitBreakerTripped()); + + int hits = 0; + + for (int index = 0; index < 15; index++) { + hits = this.testClient.getForObject("http://badClients/timeout", + Integer.class); + } + logServerStats(LocalBadClientConfiguration.badServer); + logServerStats(LocalBadClientConfiguration.badServer2); + logServerStats(LocalBadClientConfiguration.goodServer); + + assertTrue(badServer1Stats.isCircuitBreakerTripped()); + assertTrue(badServer2Stats.isCircuitBreakerTripped()); + assertTrue(!goodServerStats.isCircuitBreakerTripped()); + + // 15 + 4 timeouts. See the endpoint for timeout conditions. + assertEquals(19, hits); + + // Wait for any timeout thread to finish. + Thread.sleep(600); + + } + + @Configuration + @EnableAutoConfiguration + @RestController + @RibbonClient(name = "badClients", configuration = LocalBadClientConfiguration.class) + public static class Application { + + private AtomicInteger hits = new AtomicInteger(1); + private AtomicInteger retryHits = new AtomicInteger(1); + + @RequestMapping(method = RequestMethod.GET, value = "/ping") + public int ping() { + return 0; + } + + @RequestMapping(method = RequestMethod.GET, value = "/good") + public int good() { + int lValue = this.hits.getAndIncrement(); + return lValue; + } + + @RequestMapping(method = RequestMethod.GET, value = "/timeout") + public int timeout() throws Exception { + int lValue = this.retryHits.getAndIncrement(); + + // Force the good server to have 2 consecutive errors a couple of times. + if (lValue == 2 || lValue == 3 || lValue == 5 || lValue == 6) { + Thread.sleep(500); + } + return lValue; + } + + @RequestMapping(method = RequestMethod.GET, value = "/null") + public int isNull() throws Exception { + throw new NullPointerException("Null"); + } + + @LoadBalanced + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } + } + +} + +// Load balancer with fixed server list for "local" pointing to localhost +// and some bogus servers are thrown in to test retry +@Configuration +class LocalBadClientConfiguration { + + static BaseLoadBalancer balancer; + static Server goodServer; + static Server badServer; + static Server badServer2; + + public LocalBadClientConfiguration() { + } + + @Value("${local.server.port}") + private int port = 0; + + @Bean + public IRule loadBalancerRule() { + // This is a good place to try different load balancing rules and how those rules + // behave in failure states: BestAvailableRule, WeightedResponseTimeRule, etc + + // This rule just uses a round robin and will skip servers that are in circuit + // breaker state. + return new AvailabilityFilteringRule(); + + } + + @Bean + public ILoadBalancer ribbonLoadBalancer(IClientConfig config, + ServerList serverList, IRule rule, IPing ping) { + + goodServer = new Server("localhost", this.port); + badServer = new Server("mybadhost", 10001); + badServer2 = new Server("localhost", SocketUtils.findAvailableTcpPort()); + + balancer = LoadBalancerBuilder.newBuilder().withClientConfig(config) + .withRule(rule).withPing(ping).buildFixedServerListLoadBalancer( + Arrays.asList(badServer, badServer2, goodServer)); + return balancer; + } + + @Bean + public RetryHandler retryHandler() { + return new OverrideRetryHandler(); + } + + static class OverrideRetryHandler extends HttpClientLoadBalancerErrorHandler { + public OverrideRetryHandler() { + this.circuitRelated.add(UnknownHostException.class); + this.retriable.add(UnknownHostException.class); + } + } + +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java index 9b521676..940634ff 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java @@ -90,6 +90,34 @@ public class PreDecorationFilterTests { assertEquals(false, this.filter.shouldFilter()); } + @Test + public void xForwardedHostHasPort() throws Exception { + this.properties.setPrefix("/api"); + this.request.setRequestURI("/api/foo/1"); + this.request.setRemoteAddr("5.6.7.8"); + this.request.setServerPort(8080); + this.routeLocator.addRoute( + new ZuulRoute("foo", "/foo/**", "foo", null, false, null, null)); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + assertEquals("localhost:8080", ctx.getZuulRequestHeaders().get("x-forwarded-host")); + } + + @Test + public void hostHeaderSet() throws Exception { + this.properties.setPrefix("/api"); + this.properties.setAddHostHeader(true); + this.request.setRequestURI("/api/foo/1"); + this.request.setRemoteAddr("5.6.7.8"); + this.request.setServerPort(8080); + this.routeLocator.addRoute( + new ZuulRoute("foo", "/foo/**", "foo", null, false, null, null)); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + assertEquals("localhost:8080", ctx.getZuulRequestHeaders().get("x-forwarded-host")); + assertEquals("localhost:8080", ctx.getZuulRequestHeaders().get("host")); + } + @Test public void prefixRouteAddsHeader() throws Exception { this.properties.setPrefix("/api"); @@ -136,6 +164,86 @@ public class PreDecorationFilterTests { getHeader(ctx.getOriginResponseHeaders(), "x-zuul-serviceid")); } + @Test + public void routeWithContextPath() { + this.properties.setStripPrefix(false); + this.request.setRequestURI("/api/foo/1"); + this.request.setContextPath("/context-path"); + this.routeLocator.addRoute( + new ZuulRoute("foo", "/api/foo/**", "foo", null, false, null, null)); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + assertEquals("/api/foo/1", ctx.get("requestURI")); + assertEquals("localhost", ctx.getZuulRequestHeaders().get("x-forwarded-host")); + assertEquals("80", ctx.getZuulRequestHeaders().get("x-forwarded-port")); + assertEquals("http", ctx.getZuulRequestHeaders().get("x-forwarded-proto")); + assertEquals("/context-path", + ctx.getZuulRequestHeaders().get("x-forwarded-prefix")); + assertEquals("foo", + getHeader(ctx.getOriginResponseHeaders(), "x-zuul-serviceid")); + } + + @Test + public void prefixRouteWithContextPath() { + this.properties.setPrefix("/api"); + this.properties.setStripPrefix(true); + this.request.setRequestURI("/api/foo/1"); + this.request.setContextPath("/context-path"); + this.routeLocator.addRoute( + new ZuulRoute("foo", "/foo/**", "foo", null, false, null, null)); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + assertEquals("/foo/1", ctx.get("requestURI")); + assertEquals("localhost", ctx.getZuulRequestHeaders().get("x-forwarded-host")); + assertEquals("80", ctx.getZuulRequestHeaders().get("x-forwarded-port")); + assertEquals("http", ctx.getZuulRequestHeaders().get("x-forwarded-proto")); + assertEquals("/context-path/api", + ctx.getZuulRequestHeaders().get("x-forwarded-prefix")); + assertEquals("foo", + getHeader(ctx.getOriginResponseHeaders(), "x-zuul-serviceid")); + } + + @Test + public void routeIgnoreContextPathIfPrefixHeader() { + this.properties.setStripPrefix(false); + this.request.setRequestURI("/api/foo/1"); + this.request.setContextPath("/context-path"); + this.request.addHeader("X-Forwarded-Prefix", "/prefix"); + this.routeLocator.addRoute( + new ZuulRoute("foo", "/api/foo/**", "foo", null, false, null, null)); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + assertEquals("/api/foo/1", ctx.get("requestURI")); + assertEquals("localhost", ctx.getZuulRequestHeaders().get("x-forwarded-host")); + assertEquals("80", ctx.getZuulRequestHeaders().get("x-forwarded-port")); + assertEquals("http", ctx.getZuulRequestHeaders().get("x-forwarded-proto")); + assertEquals("/prefix", + ctx.getZuulRequestHeaders().get("x-forwarded-prefix")); + assertEquals("foo", + getHeader(ctx.getOriginResponseHeaders(), "x-zuul-serviceid")); + } + + @Test + public void prefixRouteIgnoreContextPathIfPrefixHeader() { + this.properties.setPrefix("/api"); + this.properties.setStripPrefix(true); + this.request.setRequestURI("/api/foo/1"); + this.request.setContextPath("/context-path"); + this.request.addHeader("X-Forwarded-Prefix", "/prefix"); + this.routeLocator.addRoute( + new ZuulRoute("foo", "/foo/**", "foo", null, false, null, null)); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + assertEquals("/foo/1", ctx.get("requestURI")); + assertEquals("localhost", ctx.getZuulRequestHeaders().get("x-forwarded-host")); + assertEquals("80", ctx.getZuulRequestHeaders().get("x-forwarded-port")); + assertEquals("http", ctx.getZuulRequestHeaders().get("x-forwarded-proto")); + assertEquals("/prefix/api", + ctx.getZuulRequestHeaders().get("x-forwarded-prefix")); + assertEquals("foo", + getHeader(ctx.getOriginResponseHeaders(), "x-zuul-serviceid")); + } + @Test public void forwardRouteAddsLocation() throws Exception { this.properties.setPrefix("/api"); @@ -377,7 +485,7 @@ public class PreDecorationFilterTests { assertTrue("sensitiveHeaders is wrong: " + sensitiveHeaders, sensitiveHeaders.containsAll(Arrays.asList("x-bar", "x-foo"))); } - + @Test public void urlProperlyDecodedWhenCharacterEncodingIsSet() throws Exception { this.request.setCharacterEncoding("UTF-8"); 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 3557179f..8378efc8 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 @@ -26,7 +26,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.AllNestedConditions; import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -204,7 +203,8 @@ public class EurekaClientAutoConfiguration { @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented - @Conditional(OnRefreshScopeCondition.class) + @ConditionalOnClass(RefreshScope.class) + @ConditionalOnBean(RefreshAutoConfiguration.class) @interface ConditionalOnRefreshScope { } @@ -219,24 +219,10 @@ public class EurekaClientAutoConfiguration { static class MissingClass { } - @ConditionalOnClass(RefreshScope.class) @ConditionalOnMissingBean(RefreshAutoConfiguration.class) static class MissingScope { } } - private static class OnRefreshScopeCondition extends AllNestedConditions { - - public OnRefreshScopeCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnClass(RefreshScope.class) - @ConditionalOnBean(RefreshAutoConfiguration.class) - static class FoundScope { - } - - } - } 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 a9240293..9363bec9 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 @@ -112,8 +112,8 @@ public class EurekaClientAutoConfigurationTests { EnvironmentTestUtils.addEnvironment(this.context, "server.port=8989", "eureka.client.serviceUrl.defaultZone=http://user:foo@example.com:80/eureka"); setupContext(MockClientConfiguration.class); - //ApacheHttpClient4 http = this.context.getBean(ApacheHttpClient4.class); - //Mockito.verify(http).addFilter(Matchers.any(HTTPBasicAuthFilter.class)); + // ApacheHttpClient4 http = this.context.getBean(ApacheHttpClient4.class); + // Mockito.verify(http).addFilter(Matchers.any(HTTPBasicAuthFilter.class)); } private void testNonSecurePort(String propName) {