diff --git a/pom.xml b/pom.xml index 68529471..654a4263 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,16 @@ + + org.springframework.cloud + spring-cloud-starter-hystrix + true + + + io.reactivex + rxjava-reactive-streams + 1.2.1 + org.springframework.boot spring-boot-devtools diff --git a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index ec626f3c..bd7e3167 100644 --- a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -14,10 +14,12 @@ import org.springframework.cloud.gateway.api.RouteReader; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter; import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; +import org.springframework.cloud.gateway.filter.RoutingFilter; import org.springframework.cloud.gateway.filter.WriteResponseFilter; import org.springframework.cloud.gateway.filter.route.AddRequestHeaderRouteFilter; import org.springframework.cloud.gateway.filter.route.AddRequestParameterRouteFilter; import org.springframework.cloud.gateway.filter.route.AddResponseHeaderRouteFilter; +import org.springframework.cloud.gateway.filter.route.HystrixRouteFilter; import org.springframework.cloud.gateway.filter.route.RedirectToRouteFilter; import org.springframework.cloud.gateway.filter.route.RemoveRequestHeaderRouteFilter; import org.springframework.cloud.gateway.filter.route.RemoveResponseHeaderRouteFilter; @@ -74,10 +76,10 @@ public class GatewayAutoConfiguration { } @Bean - public FilteringWebHandler filteringWebHandler(RoutingWebHandler webHandler, + public FilteringWebHandler filteringWebHandler(//RoutingWebHandler webHandler, List globalFilters, Map routeFilters) { - return new FilteringWebHandler(webHandler, globalFilters, routeFilters); + return new FilteringWebHandler(/*webHandler,*/ globalFilters, routeFilters); } @Bean @@ -96,6 +98,11 @@ public class GatewayAutoConfiguration { return new LoadBalancerClientFilter(client); } + @Bean + public RoutingFilter routingFilter(HttpClient httpClient) { + return new RoutingFilter(httpClient); + } + @Bean public RouteToRequestUrlFilter routeToRequestUrlFilter() { return new RouteToRequestUrlFilter(); @@ -155,6 +162,11 @@ public class GatewayAutoConfiguration { return new AddResponseHeaderRouteFilter(); } + @Bean(name = "HystrixRouteFilter") + public HystrixRouteFilter hystrixRouteFilter() { + return new HystrixRouteFilter(); + } + @Bean(name = "RedirectToRouteFilter") public RedirectToRouteFilter redirectToRouteFilter() { return new RedirectToRouteFilter(); diff --git a/src/main/java/org/springframework/cloud/gateway/filter/RoutingFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/RoutingFilter.java new file mode 100644 index 00000000..7bd6054d --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/filter/RoutingFilter.java @@ -0,0 +1,75 @@ +package org.springframework.cloud.gateway.filter; + +import java.net.URI; +import java.util.Optional; + +import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.WebHandler; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; + +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import reactor.core.publisher.Mono; +import reactor.ipc.netty.NettyPipeline; +import reactor.ipc.netty.http.client.HttpClient; + +/** + * @author Spencer Gibb + */ +public class RoutingFilter implements GlobalFilter, Ordered { + + private final HttpClient httpClient; + + public RoutingFilter(HttpClient httpClient) { + this.httpClient = httpClient; + } + + @Override + public int getOrder() { + return 2000000; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + Optional requestUrl = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); + ServerHttpRequest request = exchange.getRequest(); + + final HttpMethod method = HttpMethod.valueOf(request.getMethod().toString()); + final String url = requestUrl.get().toString(); + + final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders(); + request.getHeaders().forEach(httpHeaders::set); + + return this.httpClient.request(method, url, req -> + req.options(NettyPipeline.SendOptions::flushOnEach) + .headers(httpHeaders) + .sendHeaders() + .send(request.getBody() + .map(DataBuffer::asByteBuffer) + .map(Unpooled::wrappedBuffer))) + .then(res -> { + ServerHttpResponse response = exchange.getResponse(); + // put headers and status so filters can modify the response + final HttpHeaders headers = new HttpHeaders(); + res.responseHeaders().forEach(entry -> headers.add(entry.getKey(), entry.getValue())); + + response.getHeaders().putAll(headers); + response.setStatusCode(HttpStatus.valueOf(res.status().code())); + + // Defer committing the response until all route filters have run + // Put client response as ServerWebExchange attribute and write response later WriteResponseFilter + exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res); + return Mono.empty(); + }).then(chain.filter(exchange)); + } +} diff --git a/src/main/java/org/springframework/cloud/gateway/filter/route/HystrixRouteFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/route/HystrixRouteFilter.java new file mode 100644 index 00000000..7c8a8616 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/filter/route/HystrixRouteFilter.java @@ -0,0 +1,64 @@ +package org.springframework.cloud.gateway.filter.route; + +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandKey; +import com.netflix.hystrix.HystrixObservableCommand; + +import reactor.core.publisher.Mono; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.Subscription; + +/** + * @author Spencer Gibb + */ +public class HystrixRouteFilter implements RouteFilter { + + @Override + public WebFilter apply(String commandName, String[] args) { + //validate(args, 1); + + final HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey(getClass().getSimpleName()); + final HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey(commandName); + + final HystrixObservableCommand.Setter setter = HystrixObservableCommand.Setter + .withGroupKey(groupKey) + .andCommandKey(commandKey); + + //TODO: caching can happen here + return (exchange, chain) -> { + try { + RouteHystrixCommand command = new RouteHystrixCommand(setter, exchange, chain); + final Observable observable = command.toObservable(); + + return Mono.create(s -> { + final Subscription subscription = observable.subscribe(s::success, s::error, s::success); + s.setCancellation(subscription::unsubscribe); + }); + } catch (Exception e) { + throw new RuntimeException("Error running RouteHystrixCommand: " + commandName, e); + } + }; + } + + //TODO: replace with HystrixMonoCommand that we write + private class RouteHystrixCommand extends HystrixObservableCommand { + private final ServerWebExchange exchange; + private final WebFilterChain chain; + + protected RouteHystrixCommand(Setter setter, ServerWebExchange exchange, WebFilterChain chain) { + super(setter); + this.exchange = exchange; + this.chain = chain; + } + + @Override + protected Observable construct() { + return RxReactiveStreams.toObservable(this.chain.filter(this.exchange)); + } + } +} diff --git a/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java b/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java index b95f834c..032e9c8d 100644 --- a/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java +++ b/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java @@ -58,6 +58,11 @@ public class FilteringWebHandler extends WebHandlerDecorator { private final List globalFilters; private final Map routeFilters = new HashMap<>(); + public FilteringWebHandler(List globalFilters, + Map routeFilters) { + this(new EmptyWebHandler(), globalFilters, routeFilters); + } + public FilteringWebHandler(WebHandler targetHandler, List globalFilters, Map routeFilters) { super(targetHandler); @@ -92,6 +97,8 @@ public class FilteringWebHandler extends WebHandlerDecorator { AnnotationAwareOrderComparator.sort(routeFilters); + logger.debug("Sorted routeFilters: "+ routeFilters); + return new DefaultWebFilterChain(routeFilters, getDelegate()).filter(exchange); } @@ -211,4 +218,11 @@ public class FilteringWebHandler extends WebHandlerDecorator { } } + private static class EmptyWebHandler implements WebHandler { + @Override + public Mono handle(ServerWebExchange exchange) { + return Mono.empty(); + } + } + } diff --git a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index e42d56c4..2e3202b6 100644 --- a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -210,6 +210,44 @@ public class GatewayIntegrationTests { ); } + @Test + public void hystrixFilterWorks() { + Mono result = webClient.exchange( + GET("http://localhost:" + port + "/get") + .header("Host", "www.hystrixsuccess.org") + .build() + ); + + verify( () -> + StepVerifier.create(result) + .consumeNextWith( + response -> { + HttpHeaders httpHeaders = response.headers().asHttpHeaders(); + assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER)) + .isEqualTo("hystrix_success_test"); + HttpStatus statusCode = response.statusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.OK); + }) + .expectComplete() + .verify(DURATION) + ); + } + + @Test + public void hystrixFilterTimesout() { + Mono result = webClient.exchange( + GET("http://localhost:" + port + "/delay/3") + .header("Host", "www.hystrixfailure.org") + .build() + ); + + verify( () -> + StepVerifier.create(result) + .expectError() + .verify() + ); + } + @Test public void loadBalancerFilterWorks() { Mono result = webClient.exchange( @@ -229,7 +267,7 @@ public class GatewayIntegrationTests { assertThat(statusCode).isEqualTo(HttpStatus.OK); }) .expectComplete() - .verify(Duration.ofMinutes(5)) + .verify(DURATION) ); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 11c76c04..e9b59078 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -47,6 +47,22 @@ spring: filters: - AddResponseHeader=X-Request-Foo, Bar + # ===================================== + - id: hystrix_failure_test + uri: http://httpbin.org:80 + predicates: + - Host=**.hystrixfailure.org + filters: + - Hystrix=failcmd + + # ===================================== + - id: hystrix_success_test + uri: http://httpbin.org:80 + predicates: + - Host=**.hystrixsuccess.org + filters: + - Hystrix=successcmd + # ===================================== - id: load_balancer_client_test uri: lb://myservice @@ -141,6 +157,8 @@ myservice: NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList listOfServers: httpbin.org:80 +#hystrix.command.failcmd.execution.isolation.thread.timeoutInMilliseconds: 1000 + logging: level: org.springframework.cloud.gateway: TRACE