From 87e8892fe4f34c609daedd48c8658d33e4b64516 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Tue, 14 Jul 2020 10:53:00 -0400 Subject: [PATCH] Add the ability to trip the circuit breaker based on returned status code. --- .../main/asciidoc/spring-cloud-gateway.adoc | 49 +++++++++++++++++++ .../factory/RetryGatewayFilterFactory.java | 16 +++--- ...pringCloudCircuitBreakerFilterFactory.java | 45 ++++++++++++++++- .../support/ServerWebExchangeUtils.java | 11 +++++ ...CloudCircuitBreakerFilterFactoryTests.java | 18 +++++++ .../SpringCloudCircuitBreakerTestConfig.java | 6 +++ .../src/test/resources/application.yml | 14 ++++++ 7 files changed, 148 insertions(+), 11 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 25130f29..e40b4349 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -831,6 +831,55 @@ It is added to the `ServerWebExchange` as the `ServerWebExchangeUtils.CIRCUITBRE For the external controller/handler scenario, headers can be added with exception details. You can find more information on doing so in the <>. +[[circuit-breaker-status-codes]] +==== Tripping The Circuit Breaker On Status Codes + +In some cases you might want to trip a circuit breaker based on the status code +returned from the route it wraps. The circuit breaker config object takes a list of +status codes that if returned will cause the the circuit breaker to be tripped. When setting the +status codes you want to trip the circuit breaker you can either use a integer with the status code +value or the String representation of the `HttpStatus` enumeration. + +.application.yml +==== +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingServiceEndpoint + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/inCaseOfFailureUseThis + statusCodes: + - 500 + - "NOT_FOUND" +---- +==== + +.Application.java +==== +[source,java] +---- +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint") + .filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis").addStatusCode("INTERNAL_SERVER_ERROR")) + .rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088") + .build(); +} +---- +==== + + + [[fallback-headers]] === The `FallbackHeaders` `GatewayFilter` Factory diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java index 50cf4292..71f135b3 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java @@ -20,9 +20,7 @@ import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Set; import java.util.function.Predicate; import java.util.function.Supplier; @@ -40,6 +38,7 @@ import org.springframework.cloud.gateway.event.EnableBodyCachingEvent; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.support.HasRouteId; +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.cloud.gateway.support.TimeoutException; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -48,8 +47,6 @@ import org.springframework.util.Assert; import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; -import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_HEADER_NAMES; -import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.removeAlreadyRouted; public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory { @@ -211,13 +208,12 @@ public class RetryGatewayFilterFactory return exceeds; } + @Deprecated + /** + * Use {@link ServerWebExchangeUtils#reset(ServerWebExchange)} + */ public void reset(ServerWebExchange exchange) { - // TODO: what else to do to reset exchange? - Set addedHeaders = exchange.getAttributeOrDefault( - CLIENT_RESPONSE_HEADER_NAMES, Collections.emptySet()); - addedHeaders - .forEach(header -> exchange.getResponse().getHeaders().remove(header)); - removeAlreadyRouted(exchange); + ServerWebExchangeUtils.reset(exchange); } @Deprecated diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactory.java index 129a9e88..79461968 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactory.java @@ -17,7 +17,10 @@ package org.springframework.cloud.gateway.filter.factory; import java.net.URI; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import reactor.core.publisher.Mono; @@ -27,8 +30,11 @@ import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFac import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.support.HasRouteId; +import org.springframework.cloud.gateway.support.HttpStatusHolder; +import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; @@ -40,6 +46,7 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.C import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.removeAlreadyRouted; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset; /** * @author Ryan Baxter @@ -85,7 +92,19 @@ public abstract class SpringCloudCircuitBreakerFilterFactory extends @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - return cb.run(chain.filter(exchange), t -> { + return cb.run(chain.filter(exchange).doOnSuccess(v -> { + Set statuses = config.getStatusCodes().stream() + .map(HttpStatusHolder::parse) + .filter(statusHolder -> statusHolder.getHttpStatus() != null) + .map(HttpStatusHolder::getHttpStatus) + .collect(Collectors.toSet()); + if (statuses.contains(exchange.getResponse().getStatusCode())) { + HttpStatus status = exchange.getResponse().getStatusCode(); + exchange.getResponse().setStatusCode(null); + reset(exchange); + throw new CircuitBreakerStatusCodeException(status); + } + }), t -> { if (config.getFallbackUri() == null) { return Mono.error(t); } @@ -141,6 +160,8 @@ public abstract class SpringCloudCircuitBreakerFilterFactory extends private String routeId; + private Set statusCodes = new HashSet<>(); + @Override public void setRouteId(String routeId) { this.routeId = routeId; @@ -179,6 +200,28 @@ public abstract class SpringCloudCircuitBreakerFilterFactory extends return name; } + public Set getStatusCodes() { + return statusCodes; + } + + public Config setStatusCodes(Set statusCodes) { + this.statusCodes = statusCodes; + return this; + } + + public Config addStatusCode(String statusCode) { + this.statusCodes.add(statusCode); + return this; + } + + } + + public class CircuitBreakerStatusCodeException extends HttpStatusCodeException { + + public CircuitBreakerStatusCodeException(HttpStatus statusCode) { + super(statusCode); + } + } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java index 62cbfc86..20a77f67 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java @@ -17,9 +17,11 @@ package org.springframework.cloud.gateway.support; import java.net.URI; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; @@ -193,6 +195,15 @@ public final class ServerWebExchangeUtils { return response; } + public static void reset(ServerWebExchange exchange) { + // TODO: what else to do to reset exchange? + Set addedHeaders = exchange.getAttributeOrDefault( + CLIENT_RESPONSE_HEADER_NAMES, Collections.emptySet()); + addedHeaders + .forEach(header -> exchange.getResponse().getHeaders().remove(header)); + removeAlreadyRouted(exchange); + } + public static boolean setResponseStatus(ServerWebExchange exchange, HttpStatusHolder statusHolder) { if (exchange.getResponse().isCommitted()) { diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactoryTests.java index 77fbff73..7d750cc8 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactoryTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactoryTests.java @@ -104,4 +104,22 @@ public abstract class SpringCloudCircuitBreakerFilterFactoryTests .json("{\"from\":\"circuitbreakerfallbackcontroller3\"}"); } + @Test + public void filterStatusCodeFallback() { + testClient.get().uri("/status/500") + .header("Host", "www.circuitbreakerstatuscode.org").exchange() + .expectStatus().isOk().expectBody() + .json("{\"from\":\"statusCodeFallbackController\"}"); + + testClient.get().uri("/status/404") + .header("Host", "www.circuitbreakerstatuscode.org").exchange() + .expectStatus().isOk().expectBody() + .json("{\"from\":\"statusCodeFallbackController\"}"); + + testClient.get().uri("/status/200") + .header("Host", "www.circuitbreakerstatuscode.org").exchange() + .expectStatus().isOk().expectHeader() + .valueEquals(ROUTE_ID_HEADER, "circuitbreaker_fallback_test_statuscode"); + } + } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerTestConfig.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerTestConfig.java index 070ab1f7..24b9979b 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerTestConfig.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerTestConfig.java @@ -36,6 +36,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; @@ -69,6 +70,11 @@ public class SpringCloudCircuitBreakerTestConfig { return Collections.singletonMap("from", "circuitbreakerfallbackcontroller3"); } + @RequestMapping("/statusCodeFallbackController") + public Map statusCodeFallbackController(ServerWebExchange exchange) { + return Collections.singletonMap("from", "statusCodeFallbackController"); + } + @Bean public RouteLocator circuitBreakerRouteLocator(RouteLocatorBuilder builder) { return builder.routes().route("circuitbreaker_fallback_forward", diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 455782b7..e00d030b 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -94,6 +94,20 @@ spring: name: fallbackcmd fallbackUri: forward:/circuitbreakerFallbackController + # ===================================== + - id: circuitbreaker_fallback_test_statuscode + uri: ${test.uri} + predicates: + - Host=**.circuitbreakerstatuscode.org + filters: + - name: CircuitBreaker + args: + name: fallbackcmd + statusCodes: + - 500 + - "NOT_FOUND" + fallbackUri: forward:/statusCodeFallbackController + # ===================================== - id: change_uri_test uri: ${test.uri}