Add the ability to trip the circuit breaker based on returned status code.

This commit is contained in:
Ryan Baxter
2020-07-14 10:53:00 -04:00
parent d165617d92
commit 87e8892fe4
7 changed files with 148 additions and 11 deletions

View File

@@ -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 <<fallback-headers, FallbackHeaders GatewayFilter Factory section>>.
[[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

View File

@@ -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<RetryGatewayFilterFactory.RetryConfig> {
@@ -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<String> addedHeaders = exchange.getAttributeOrDefault(
CLIENT_RESPONSE_HEADER_NAMES, Collections.emptySet());
addedHeaders
.forEach(header -> exchange.getResponse().getHeaders().remove(header));
removeAlreadyRouted(exchange);
ServerWebExchangeUtils.reset(exchange);
}
@Deprecated

View File

@@ -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<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
return cb.run(chain.filter(exchange), t -> {
return cb.run(chain.filter(exchange).doOnSuccess(v -> {
Set<HttpStatus> 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<String> 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<String> getStatusCodes() {
return statusCodes;
}
public Config setStatusCodes(Set<String> 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);
}
}
}

View File

@@ -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<String> 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()) {

View File

@@ -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");
}
}

View File

@@ -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<String, String> statusCodeFallbackController(ServerWebExchange exchange) {
return Collections.singletonMap("from", "statusCodeFallbackController");
}
@Bean
public RouteLocator circuitBreakerRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("circuitbreaker_fallback_forward",

View File

@@ -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}