Commit 00b689f5 by Julien Roy Committed by Dave Syer

Add retryable property on Zuul route configuration

The ProxyRouteLocator now has a retryable property (default null which means Ribbon will choose the default - usually false). Fixes gh-115, fixes gh-124
parent 9fb833e4
...@@ -747,6 +747,12 @@ the service-specific prefix from individual routes, e.g. ...@@ -747,6 +747,12 @@ the service-specific prefix from individual routes, e.g.
In this example requests to "/myusers/101" will be forwarded to "/myusers/101" on the "users" service. In this example requests to "/myusers/101" will be forwarded to "/myusers/101" on the "users" service.
The `zuul.routes` entries actually bind to an object of type `ProxyRouteLocator`. If you
look at the properties of that object you will see that it also has a "retryable" flag.
Set that flag to "true" to have the Ribbon client automatically retry failed requests
(and if you need to you can modify the parameters of the retry operations using
the Ribbon client configuration).
The `X-Forwarded-Host` header added to the forwarded requests by The `X-Forwarded-Host` header added to the forwarded requests by
default. To turn it off set `zuul.addProxyHeaders = false`. The default. To turn it off set `zuul.addProxyHeaders = false`. The
prefix path is stripped by default, and the request to the backend prefix path is stripped by default, and the request to the backend
......
...@@ -88,6 +88,7 @@ public class ProxyRouteLocator implements RouteLocator { ...@@ -88,6 +88,7 @@ public class ProxyRouteLocator implements RouteLocator {
String targetPath = null; String targetPath = null;
String id = null; String id = null;
String prefix = this.properties.getPrefix(); String prefix = this.properties.getPrefix();
Boolean retryable = this.properties.getRetryable();
for (Entry<String, ZuulRoute> entry : this.routes.get().entrySet()) { for (Entry<String, ZuulRoute> entry : this.routes.get().entrySet()) {
String pattern = entry.getKey(); String pattern = entry.getKey();
if (this.pathMatcher.match(pattern, path)) { if (this.pathMatcher.match(pattern, path)) {
...@@ -106,11 +107,14 @@ public class ProxyRouteLocator implements RouteLocator { ...@@ -106,11 +107,14 @@ public class ProxyRouteLocator implements RouteLocator {
prefix = prefix + routePrefix; prefix = prefix + routePrefix;
} }
} }
if(route.getRetryable() != null) {
retryable = route.getRetryable();
}
break; break;
} }
} }
return (location == null ? null : new ProxyRouteSpec(id, targetPath, location, return (location == null ? null : new ProxyRouteSpec(id, targetPath, location,
prefix)); prefix, retryable));
} }
public void resetRoutes() { public void resetRoutes() {
...@@ -188,6 +192,8 @@ public class ProxyRouteLocator implements RouteLocator { ...@@ -188,6 +192,8 @@ public class ProxyRouteLocator implements RouteLocator {
private String prefix; private String prefix;
private Boolean retryable;
} }
} }
...@@ -43,6 +43,8 @@ public class ZuulProperties { ...@@ -43,6 +43,8 @@ public class ZuulProperties {
private boolean stripPrefix = true; private boolean stripPrefix = true;
private Boolean retryable;
private Map<String, ZuulRoute> routes = new LinkedHashMap<String, ZuulRoute>(); private Map<String, ZuulRoute> routes = new LinkedHashMap<String, ZuulRoute>();
private boolean addProxyHeaders = true; private boolean addProxyHeaders = true;
...@@ -80,6 +82,8 @@ public class ZuulProperties { ...@@ -80,6 +82,8 @@ public class ZuulProperties {
private boolean stripPrefix = true; private boolean stripPrefix = true;
private Boolean retryable;
public ZuulRoute(String text) { public ZuulRoute(String text) {
String location = null; String location = null;
String path = text; String path = text;
......
...@@ -67,6 +67,11 @@ public class PreDecorationFilter extends ZuulFilter { ...@@ -67,6 +67,11 @@ public class PreDecorationFilter extends ZuulFilter {
if (location != null) { if (location != null) {
ctx.put("requestURI", route.getPath()); ctx.put("requestURI", route.getPath());
ctx.put("proxy", route.getId()); ctx.put("proxy", route.getId());
if (route.getRetryable() != null) {
ctx.put("retryable", route.getRetryable());
}
if (location.startsWith("http:") || location.startsWith("https:")) { if (location.startsWith("http:") || location.startsWith("https:")) {
ctx.setRouteHost(getUrl(location)); ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader("X-Zuul-Service", location); ctx.addOriginResponseHeader("X-Zuul-Service", location);
......
...@@ -53,6 +53,8 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> { ...@@ -53,6 +53,8 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> {
private URI uri; private URI uri;
private Boolean retryable;
private MultivaluedMap<String, String> headers; private MultivaluedMap<String, String> headers;
private MultivaluedMap<String, String> params; private MultivaluedMap<String, String> params;
...@@ -60,13 +62,15 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> { ...@@ -60,13 +62,15 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> {
private InputStream requestEntity; private InputStream requestEntity;
public RibbonCommand(RestClient restClient, Verb verb, String uri, public RibbonCommand(RestClient restClient, Verb verb, String uri,
Boolean retryable,
MultivaluedMap<String, String> headers, MultivaluedMap<String, String> headers,
MultivaluedMap<String, String> params, InputStream requestEntity) MultivaluedMap<String, String> params, InputStream requestEntity)
throws URISyntaxException { throws URISyntaxException {
this("default", restClient, verb, uri, headers, params, requestEntity); this("default", restClient, verb, uri, retryable , headers, params, requestEntity);
} }
public RibbonCommand(String commandKey, RestClient restClient, Verb verb, String uri, public RibbonCommand(String commandKey, RestClient restClient, Verb verb, String uri,
Boolean retryable,
MultivaluedMap<String, String> headers, MultivaluedMap<String, String> headers,
MultivaluedMap<String, String> params, InputStream requestEntity) MultivaluedMap<String, String> params, InputStream requestEntity)
throws URISyntaxException { throws URISyntaxException {
...@@ -74,6 +78,7 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> { ...@@ -74,6 +78,7 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> {
this.restClient = restClient; this.restClient = restClient;
this.verb = verb; this.verb = verb;
this.uri = new URI(uri); this.uri = new URI(uri);
this.retryable = retryable;
this.headers = headers; this.headers = headers;
this.params = params; this.params = params;
this.requestEntity = requestEntity; this.requestEntity = requestEntity;
...@@ -102,6 +107,11 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> { ...@@ -102,6 +107,11 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> {
RequestContext context = RequestContext.getCurrentContext(); RequestContext context = RequestContext.getCurrentContext();
Builder builder = HttpRequest.newBuilder().verb(this.verb).uri(this.uri) Builder builder = HttpRequest.newBuilder().verb(this.verb).uri(this.uri)
.entity(this.requestEntity); .entity(this.requestEntity);
if(retryable != null) {
builder.setRetriable(retryable);
}
for (String name : this.headers.keySet()) { for (String name : this.headers.keySet()) {
List<String> values = this.headers.get(name); List<String> values = this.headers.get(name);
for (String value : values) { for (String value : values) {
......
...@@ -94,6 +94,7 @@ public class RibbonRoutingFilter extends ZuulFilter { ...@@ -94,6 +94,7 @@ public class RibbonRoutingFilter extends ZuulFilter {
InputStream requestEntity = getRequestBody(request); InputStream requestEntity = getRequestBody(request);
String serviceId = (String) context.get("serviceId"); String serviceId = (String) context.get("serviceId");
Boolean retryable = (Boolean) context.get("retryable");
RestClient restClient = this.clientFactory.getClient(serviceId, RestClient.class); RestClient restClient = this.clientFactory.getClient(serviceId, RestClient.class);
...@@ -106,7 +107,7 @@ public class RibbonRoutingFilter extends ZuulFilter { ...@@ -106,7 +107,7 @@ public class RibbonRoutingFilter extends ZuulFilter {
String service = (String) context.get("serviceId"); String service = (String) context.get("serviceId");
try { try {
HttpResponse response = forward(restClient, service, verb, uri, headers, params, HttpResponse response = forward(restClient, service, verb, uri, retryable, headers, params,
requestEntity); requestEntity);
setResponse(response); setResponse(response);
return response; return response;
...@@ -118,12 +119,12 @@ public class RibbonRoutingFilter extends ZuulFilter { ...@@ -118,12 +119,12 @@ public class RibbonRoutingFilter extends ZuulFilter {
return null; return null;
} }
private HttpResponse forward(RestClient restClient, String service, Verb verb, String uri, private HttpResponse forward(RestClient restClient, String service, Verb verb, String uri, Boolean retryable,
MultiValueMap<String, String> headers, MultiValueMap<String, String> params, MultiValueMap<String, String> headers, MultiValueMap<String, String> params,
InputStream requestEntity) throws Exception { InputStream requestEntity) throws Exception {
Map<String, Object> info = this.helper.debug(verb.verb(), uri, headers, params, Map<String, Object> info = this.helper.debug(verb.verb(), uri, headers, params,
requestEntity); requestEntity);
RibbonCommand command = new RibbonCommand(service, restClient, verb, uri, RibbonCommand command = new RibbonCommand(service, restClient, verb, uri, retryable,
convertHeaders(headers), convertHeaders(params), requestEntity); convertHeaders(headers), convertHeaders(params), requestEntity);
try { try {
HttpResponse response = command.execute(); HttpResponse response = command.execute();
......
package org.springframework.cloud.netflix.zuul;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
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.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.zuul.ZuulFilter;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = RetryableZuulProxyApplication.class)
@WebAppConfiguration
@IntegrationTest({ "server.port: 0",
"zuul.routes.simple.path: /simple/**",
"zuul.routes.simple.retryable: true",
"ribbon.OkToRetryOnAllOperations: true"
})
@DirtiesContext
public class RetryableZuulProxyApplicationTests {
@Value("${local.server.port}")
private int port;
@Autowired
private ProxyRouteLocator routes;
@Autowired
private RoutesEndpoint endpoint;
@Test
public void postWithForm() {
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
form.set("foo", "bar");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + port + "/simple", HttpMethod.POST,
new HttpEntity<MultiValueMap<String,String>>(form, headers), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Posted! {foo=[bar]}", result.getBody());
}
}
//Don't use @SpringBootApplication because we don't want to component scan
@Configuration
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
@RibbonClient(name = "simple", configuration = RetryableRibbonClientConfiguration.class)
class RetryableZuulProxyApplication {
@RequestMapping(value = "/", method = RequestMethod.POST)
public String delete(@RequestBody MultiValueMap<String, String> form) {
return "Posted! " + form;
}
@Bean
public ZuulFilter sampleFilter() {
return new ZuulFilter() {
@Override
public String filterType() {
return "pre";
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
return null;
}
@Override
public int filterOrder() {
return 0;
}
};
}
public static void main(String[] args) {
SpringApplication.run(SampleZuulProxyApplication.class, args);
}
}
//Load balancer with fixed server list for "simple" pointing to localhost
@Configuration
class RetryableRibbonClientConfiguration {
@Bean
public ILoadBalancer ribbonLoadBalancer(EurekaInstanceConfig instance) {
BaseLoadBalancer balancer = new BaseLoadBalancer();
balancer.setServersList(Arrays.asList(
new Server("localhost", instance.getNonSecurePort()),
new Server("failed-localhost", instance.getNonSecurePort())
));
return balancer;
}
}
\ No newline at end of file
...@@ -89,7 +89,7 @@ public class ProxyRouteLocatorTests { ...@@ -89,7 +89,7 @@ public class ProxyRouteLocatorTests {
ProxyRouteLocator routeLocator = new ProxyRouteLocator(this.discovery, ProxyRouteLocator routeLocator = new ProxyRouteLocator(this.discovery,
this.properties); this.properties);
this.properties.getRoutes().put("foo", this.properties.getRoutes().put("foo",
new ZuulRoute("foo", "/foo/**", "foo", null, false)); new ZuulRoute("foo", "/foo/**", "foo", null, false, null));
this.properties.setStripPrefix(false); this.properties.setStripPrefix(false);
this.properties.setPrefix("/proxy"); this.properties.setPrefix("/proxy");
routeLocator.getRoutes(); // force refresh routeLocator.getRoutes(); // force refresh
...@@ -116,7 +116,7 @@ public class ProxyRouteLocatorTests { ...@@ -116,7 +116,7 @@ public class ProxyRouteLocatorTests {
ProxyRouteLocator routeLocator = new ProxyRouteLocator(this.discovery, ProxyRouteLocator routeLocator = new ProxyRouteLocator(this.discovery,
this.properties); this.properties);
this.properties.getRoutes().put("foo", this.properties.getRoutes().put("foo",
new ZuulRoute("foo", "/foo/**", "foo", null, false)); new ZuulRoute("foo", "/foo/**", "foo", null, false, null));
this.properties.setPrefix("/proxy"); this.properties.setPrefix("/proxy");
routeLocator.getRoutes(); // force refresh routeLocator.getRoutes(); // force refresh
ProxyRouteSpec route = routeLocator.getMatchingRoute("/proxy/foo/1"); ProxyRouteSpec route = routeLocator.getMatchingRoute("/proxy/foo/1");
......
...@@ -70,7 +70,7 @@ public class PreDecorationFilterTests { ...@@ -70,7 +70,7 @@ public class PreDecorationFilterTests {
this.properties.setPrefix("/api"); this.properties.setPrefix("/api");
this.properties.setStripPrefix(true); this.properties.setStripPrefix(true);
this.request.setRequestURI("/api/foo/1"); this.request.setRequestURI("/api/foo/1");
this.routeLocator.addRoute(new ZuulRoute("foo", "/foo/**", "foo", null, false)); this.routeLocator.addRoute(new ZuulRoute("foo", "/foo/**", "foo", null, false, null));
this.filter.run(); this.filter.run();
RequestContext ctx = RequestContext.getCurrentContext(); RequestContext ctx = RequestContext.getCurrentContext();
assertEquals("/foo/1", ctx.get("requestURI")); assertEquals("/foo/1", ctx.get("requestURI"));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment