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.
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
default. To turn it off set `zuul.addProxyHeaders = false`. The
prefix path is stripped by default, and the request to the backend
......
......@@ -88,6 +88,7 @@ public class ProxyRouteLocator implements RouteLocator {
String targetPath = null;
String id = null;
String prefix = this.properties.getPrefix();
Boolean retryable = this.properties.getRetryable();
for (Entry<String, ZuulRoute> entry : this.routes.get().entrySet()) {
String pattern = entry.getKey();
if (this.pathMatcher.match(pattern, path)) {
......@@ -106,11 +107,14 @@ public class ProxyRouteLocator implements RouteLocator {
prefix = prefix + routePrefix;
}
}
if(route.getRetryable() != null) {
retryable = route.getRetryable();
}
break;
}
}
return (location == null ? null : new ProxyRouteSpec(id, targetPath, location,
prefix));
prefix, retryable));
}
public void resetRoutes() {
......@@ -188,6 +192,8 @@ public class ProxyRouteLocator implements RouteLocator {
private String prefix;
private Boolean retryable;
}
}
......@@ -43,6 +43,8 @@ public class ZuulProperties {
private boolean stripPrefix = true;
private Boolean retryable;
private Map<String, ZuulRoute> routes = new LinkedHashMap<String, ZuulRoute>();
private boolean addProxyHeaders = true;
......@@ -80,6 +82,8 @@ public class ZuulProperties {
private boolean stripPrefix = true;
private Boolean retryable;
public ZuulRoute(String text) {
String location = null;
String path = text;
......
......@@ -67,6 +67,11 @@ public class PreDecorationFilter extends ZuulFilter {
if (location != null) {
ctx.put("requestURI", route.getPath());
ctx.put("proxy", route.getId());
if (route.getRetryable() != null) {
ctx.put("retryable", route.getRetryable());
}
if (location.startsWith("http:") || location.startsWith("https:")) {
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader("X-Zuul-Service", location);
......
......@@ -53,6 +53,8 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> {
private URI uri;
private Boolean retryable;
private MultivaluedMap<String, String> headers;
private MultivaluedMap<String, String> params;
......@@ -60,13 +62,15 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> {
private InputStream requestEntity;
public RibbonCommand(RestClient restClient, Verb verb, String uri,
Boolean retryable,
MultivaluedMap<String, String> headers,
MultivaluedMap<String, String> params, InputStream requestEntity)
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,
Boolean retryable,
MultivaluedMap<String, String> headers,
MultivaluedMap<String, String> params, InputStream requestEntity)
throws URISyntaxException {
......@@ -74,6 +78,7 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> {
this.restClient = restClient;
this.verb = verb;
this.uri = new URI(uri);
this.retryable = retryable;
this.headers = headers;
this.params = params;
this.requestEntity = requestEntity;
......@@ -102,6 +107,11 @@ public class RibbonCommand extends HystrixCommand<HttpResponse> {
RequestContext context = RequestContext.getCurrentContext();
Builder builder = HttpRequest.newBuilder().verb(this.verb).uri(this.uri)
.entity(this.requestEntity);
if(retryable != null) {
builder.setRetriable(retryable);
}
for (String name : this.headers.keySet()) {
List<String> values = this.headers.get(name);
for (String value : values) {
......
......@@ -94,6 +94,7 @@ public class RibbonRoutingFilter extends ZuulFilter {
InputStream requestEntity = getRequestBody(request);
String serviceId = (String) context.get("serviceId");
Boolean retryable = (Boolean) context.get("retryable");
RestClient restClient = this.clientFactory.getClient(serviceId, RestClient.class);
......@@ -106,7 +107,7 @@ public class RibbonRoutingFilter extends ZuulFilter {
String service = (String) context.get("serviceId");
try {
HttpResponse response = forward(restClient, service, verb, uri, headers, params,
HttpResponse response = forward(restClient, service, verb, uri, retryable, headers, params,
requestEntity);
setResponse(response);
return response;
......@@ -118,12 +119,12 @@ public class RibbonRoutingFilter extends ZuulFilter {
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,
InputStream requestEntity) throws Exception {
Map<String, Object> info = this.helper.debug(verb.verb(), uri, headers, params,
requestEntity);
RibbonCommand command = new RibbonCommand(service, restClient, verb, uri,
RibbonCommand command = new RibbonCommand(service, restClient, verb, uri, retryable,
convertHeaders(headers), convertHeaders(params), requestEntity);
try {
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 {
ProxyRouteLocator routeLocator = new ProxyRouteLocator(this.discovery,
this.properties);
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.setPrefix("/proxy");
routeLocator.getRoutes(); // force refresh
......@@ -116,7 +116,7 @@ public class ProxyRouteLocatorTests {
ProxyRouteLocator routeLocator = new ProxyRouteLocator(this.discovery,
this.properties);
this.properties.getRoutes().put("foo",
new ZuulRoute("foo", "/foo/**", "foo", null, false));
new ZuulRoute("foo", "/foo/**", "foo", null, false, null));
this.properties.setPrefix("/proxy");
routeLocator.getRoutes(); // force refresh
ProxyRouteSpec route = routeLocator.getMatchingRoute("/proxy/foo/1");
......
......@@ -70,7 +70,7 @@ public class PreDecorationFilterTests {
this.properties.setPrefix("/api");
this.properties.setStripPrefix(true);
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();
RequestContext ctx = RequestContext.getCurrentContext();
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