diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index e238f4348..609f35211 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -465,6 +465,11 @@ The DedupeResponseHeader filter also accepts an optional `strategy` parameter. T [[hystrix]] === Hystrix GatewayFilter Factory + +NOTE: https://cloud.spring.io/spring-cloud-netflix/multi/multi__modules_in_maintenance_mode.html[Netflix has put Hystrix in maintenance mode]. It is suggested you use the <> with Resilience4J as support for Hystrix will be removed in a future release. + + https://github.com/Netflix/Hystrix[Hystrix] is a library from Netflix that implements the https://martinfowler.com/bliki/CircuitBreaker.html[circuit breaker pattern]. The Hystrix GatewayFilter allows you to introduce circuit breakers to your gateway routes, protecting your services from cascading failures and allowing you to provide fallback responses in the event of downstream failures. @@ -554,10 +559,96 @@ To set a 5 second timeout for the example route above, the following configurati [source,yaml] hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 5000 +[[spring-cloud-circuitbreaker-filter-factory]] +=== Spring Cloud CircuitBreaker GatewayFilter Factory + +The Spring Cloud CircuitBreaker filter factory leverages the Spring Cloud CircuitBreaker APIs to wrap Gateway routes in +a circuit breaker. Spring Cloud CircuitBreaker supports two libraries which can be used with Spring Cloud Gateway, Hystrix +and Resilience4J. Since Netflix has places Hystrix in maintenance only mode we suggest you use Resilience4J. + +To enable the Spring Cloud CircuitBreaker filter you will need to either place `spring-cloud-starter-circuitbreaker-reactor-resilience4j` or +`spring-cloud-starter-netflix-hystrix` on the classpath. + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: https://example.org + filters: + - CircuitBreaker=myCircuitBreaker +---- +To configure the circuit breaker, see the configuration for the underlying circuit breaker implementation you are using. + +* https://cloud.spring.io/spring-cloud-circuitbreaker/reference/html/spring-cloud-circuitbreaker.html[Resilience4J Documentation] +* https://cloud.spring.io/spring-cloud-netflix/reference/html/[Hystrix Documentation] + +The Spring Cloud CircuitBreaker filter can also accept an optional `fallbackUri` parameter. Currently, only `forward:` schemed URIs are supported. If the fallback is called, the request will be forwarded to the controller matched by the URI. + + +.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 + - RewritePath=/consumingserviceendpoint, /backingserviceendpoint +---- +This will forward to the `/incaseoffailureusethis` URI when the circuit breaker fallback is called. Note that this example also demonstrates (optional) Spring Cloud Netflix Ribbon load-balancing via the `lb` prefix on the destination URI. + +The primary scenario is to use the `fallbackUri` to an internal controller or handler within the gateway app. +However, it is also possible to reroute the request to a controller or handler in an external application, like so: + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: CircuitBreaker + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback +---- + +In this example, there is no `fallback` endpoint or handler in the gateway application, however, there is one in another +app, registered under `http://localhost:9994`. + +In case of the request being forwarded to fallback, the Spring Cloud CircuitBreaker Gateway filter also provides the `Throwable` that has +caused it. It's added to the `ServerWebExchange` as the +`ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR` attribute that can be used when +handling the fallback within the gateway app. + +For the external controller/handler scenario, headers can be added with exception details. You can find more information +on it in the <>. + [[fallback-headers]] === FallbackHeaders GatewayFilter Factory -The `FallbackHeaders` factory allows you to add Hystrix execution exception details in headers of a request forwarded to +The `FallbackHeaders` factory allows you to add Hystrix or Spring Cloud CircuitBreaker execution exception details in headers of a request forwarded to a `fallbackUri` in an external application, like in the following scenario: .application.yml @@ -572,7 +663,7 @@ spring: predicates: - Path=//ingredients/** filters: - - name: Hystrix + - name: CircuitBreaker args: name: fetchIngredients fallbackUri: forward:/fallback @@ -586,7 +677,7 @@ spring: executionExceptionTypeHeaderName: Test-Header ---- -In this example, after an execution exception occurs while running the `HystrixCommand`, the request will be forwarded to +In this example, after an execution exception occurs while running the circuit breaker, the request will be forwarded to the `fallback` endpoint or handler in an app running on `localhost:9994`. The headers with the exception type, message and -if available- root cause exception type and message will be added to that request by the `FallbackHeaders` filter. @@ -598,7 +689,8 @@ their default values: * `rootCauseExceptionTypeHeaderName` (`"Root-Cause-Exception-Type"`) * `rootCauseExceptionMessageHeaderName` (`"Root-Cause-Exception-Message"`) -You can find more information on how Hystrix works with Gateway in the <>. +For more information of circuit beakers and the Gateway see the <> or +<>. === MapRequestHeader GatewayFilter Factory The MapRequestHeader GatewayFilter Factory takes 'fromHeader' and 'toHeader' parameters. It creates a new named header (toHeader) and the value is extracted out of an existing named header (fromHeader) from the incoming http request. If the input header does not exist then the filter has no impact. If the new named header already exists then it's values will be augmented with the new values. diff --git a/pom.xml b/pom.xml index ee2610ab2..42fe1b1e1 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ 1.8 2.2.0.BUILD-SNAPSHOT 2.2.0.BUILD-SNAPSHOT + 1.0.0.BUILD-SNAPSHOT 0.6 @@ -111,6 +112,18 @@ spring-cloud-starter-netflix-hystrix ${spring-cloud-netflix.version} + + org.springframework.cloud + spring-cloud-circuitbreaker-dependencies + ${spring-cloud-circuitbreaker.version} + pom + import + + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-reactor-resilience4j + ${spring-cloud-circuitbreaker.version} + org.springframework.boot spring-boot-devtools diff --git a/spring-cloud-gateway-core/pom.xml b/spring-cloud-gateway-core/pom.xml index 5a24ac698..44df08a87 100644 --- a/spring-cloud-gateway-core/pom.xml +++ b/spring-cloud-gateway-core/pom.xml @@ -87,6 +87,11 @@ spring-boot-autoconfigure-processor true + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-reactor-resilience4j + true + org.springframework.cloud spring-cloud-starter-netflix-eureka-client diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 5326c9ddb..702db08d5 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -710,6 +710,7 @@ public class GatewayAutoConfiguration { } @Bean + @ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class) public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() { return new FallbackHeadersGatewayFilterFactory(); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayCircuitBreakerAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayCircuitBreakerAutoConfiguration.java new file mode 100644 index 000000000..39861bd92 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayCircuitBreakerAutoConfiguration.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.config; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JAutoConfiguration; +import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; +import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerHystrixFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerResilience4JFilterFactory; +import org.springframework.cloud.netflix.hystrix.HystrixCircuitBreakerAutoConfiguration; +import org.springframework.cloud.netflix.hystrix.ReactiveHystrixCircuitBreakerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.DispatcherHandler; + +/** + * @author Ryan Baxter + */ +@Configuration +@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true) +@AutoConfigureAfter({ ReactiveResilience4JAutoConfiguration.class, + HystrixCircuitBreakerAutoConfiguration.class }) +@ConditionalOnClass({ DispatcherHandler.class, + ReactiveResilience4JAutoConfiguration.class, + HystrixCircuitBreakerAutoConfiguration.class }) +public class GatewayCircuitBreakerAutoConfiguration { + + @Configuration + @ConditionalOnClass({ ReactiveCircuitBreakerFactory.class, + ReactiveHystrixCircuitBreakerFactory.class }) + protected static class SpringCloudCircuitBreakerHystrixConfiguration { + + @Bean + @ConditionalOnBean(ReactiveHystrixCircuitBreakerFactory.class) + public SpringCloudCircuitBreakerHystrixFilterFactory springCloudCircuitBreakerHystrixFilterFactory( + ReactiveHystrixCircuitBreakerFactory reactiveCircuitBreakerFactory, + ObjectProvider dispatcherHandler) { + return new SpringCloudCircuitBreakerHystrixFilterFactory( + reactiveCircuitBreakerFactory, dispatcherHandler); + } + + @Bean + @ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class) + public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() { + return new FallbackHeadersGatewayFilterFactory(); + } + + } + + @Configuration + @ConditionalOnClass({ ReactiveCircuitBreakerFactory.class, + ReactiveResilience4JCircuitBreakerFactory.class }) + protected static class Resilience4JConfiguration { + + @Bean + @ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class) + public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() { + return new FallbackHeadersGatewayFilterFactory(); + } + + @Bean + @ConditionalOnBean(ReactiveResilience4JCircuitBreakerFactory.class) + public SpringCloudCircuitBreakerResilience4JFilterFactory springCloudCircuitBreakerResilience4JFilterFactory( + ReactiveResilience4JCircuitBreakerFactory reactiveCircuitBreakerFactory, + ObjectProvider dispatcherHandler) { + return new SpringCloudCircuitBreakerResilience4JFilterFactory( + reactiveCircuitBreakerFactory, dispatcherHandler); + } + + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/FallbackHeadersGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/FallbackHeadersGatewayFilterFactory.java index b851dc127..9a18076aa 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/FallbackHeadersGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/FallbackHeadersGatewayFilterFactory.java @@ -25,10 +25,12 @@ import org.springframework.web.server.ServerWebExchange; import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static org.apache.commons.lang.exception.ExceptionUtils.getRootCause; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR; /** * @author Olga Maciaszek-Sharma + * @author Ryan Baxter */ public class FallbackHeadersGatewayFilterFactory extends AbstractGatewayFilterFactory { @@ -45,29 +47,37 @@ public class FallbackHeadersGatewayFilterFactory @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { - ServerWebExchange filteredExchange = ofNullable( + ServerWebExchange filteredExchange = ofNullable(ofNullable( (Throwable) exchange.getAttribute(HYSTRIX_EXECUTION_EXCEPTION_ATTR)) - .map(executionException -> { - ServerHttpRequest.Builder requestBuilder = exchange - .getRequest().mutate(); - requestBuilder.header( - config.executionExceptionTypeHeaderName, - executionException.getClass().getName()); - requestBuilder.header( - config.executionExceptionMessageHeaderName, - executionException.getMessage()); - ofNullable(getRootCause(executionException)) - .ifPresent(rootCause -> { - requestBuilder.header( - config.rootCauseExceptionTypeHeaderName, - rootCause.getClass().getName()); - requestBuilder.header( - config.rootCauseExceptionMessageHeaderName, - rootCause.getMessage()); - }); - return exchange.mutate().request(requestBuilder.build()) - .build(); - }).orElse(exchange); + .orElseGet(() -> exchange.getAttribute( + CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR))) + .map(executionException -> { + ServerHttpRequest.Builder requestBuilder = exchange + .getRequest().mutate(); + requestBuilder.header( + config.executionExceptionTypeHeaderName, + executionException.getClass() + .getName()); + requestBuilder.header( + config.executionExceptionMessageHeaderName, + executionException.getMessage()); + ofNullable( + getRootCause(executionException)) + .ifPresent(rootCause -> { + requestBuilder.header( + config.rootCauseExceptionTypeHeaderName, + rootCause + .getClass() + .getName()); + requestBuilder.header( + config.rootCauseExceptionMessageHeaderName, + rootCause + .getMessage()); + }); + return exchange.mutate() + .request(requestBuilder.build()) + .build(); + }).orElse(exchange); return chain.filter(filteredExchange); }; } 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 new file mode 100644 index 000000000..b82e95f85 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactory.java @@ -0,0 +1,178 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory; + +import java.net.URI; +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.support.HasRouteId; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.UriComponentsBuilder; + +import static java.util.Collections.singletonList; +import static java.util.Optional.ofNullable; +import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts; + +/** + * @author Ryan Baxter + */ +public abstract class SpringCloudCircuitBreakerFilterFactory extends + AbstractGatewayFilterFactory { + + private ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory; + + private ReactiveCircuitBreaker cb; + + private final ObjectProvider dispatcherHandlerProvider; + + // do not use this dispatcherHandler directly, use getDispatcherHandler() instead. + private volatile DispatcherHandler dispatcherHandler; + + public SpringCloudCircuitBreakerFilterFactory( + ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, + ObjectProvider dispatcherHandlerProvider) { + super(Config.class); + this.reactiveCircuitBreakerFactory = reactiveCircuitBreakerFactory; + this.dispatcherHandlerProvider = dispatcherHandlerProvider; + } + + private DispatcherHandler getDispatcherHandler() { + if (dispatcherHandler == null) { + dispatcherHandler = dispatcherHandlerProvider.getIfAvailable(); + } + + return dispatcherHandler; + } + + @Override + public List shortcutFieldOrder() { + return singletonList(NAME_KEY); + } + + @Override + public GatewayFilter apply(Config config) { + ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(config.getId()); + + return new GatewayFilter() { + @Override + public Mono filter(ServerWebExchange exchange, + GatewayFilterChain chain) { + return cb.run(chain.filter(exchange), t -> { + if (config.getFallbackUri() == null) { + return Mono.error(t); + } + + // TODO: copied from RouteToRequestUrlFilter + URI uri = exchange.getRequest().getURI(); + // TODO: assume always? + boolean encoded = containsEncodedParts(uri); + URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null) + .port(null).uri(config.getFallbackUri()).scheme(null) + .build(encoded).toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); + addExceptionDetails(t, exchange); + + ServerHttpRequest request = exchange.getRequest().mutate() + .uri(requestUrl).build(); + return getDispatcherHandler() + .handle(exchange.mutate().request(request).build()); + }).onErrorResume(t -> handleErrorWithoutFallback(t)); + } + + @Override + public String toString() { + return filterToStringCreator(SpringCloudCircuitBreakerFilterFactory.this) + .append("name", config.getName()) + .append("fallback", config.fallbackUri).toString(); + } + }; + } + + protected abstract Mono handleErrorWithoutFallback(Throwable t); + + private void addExceptionDetails(Throwable t, ServerWebExchange exchange) { + ofNullable(t).ifPresent(exception -> exchange.getAttributes() + .put(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR, exception)); + } + + @Override + public String name() { + return "CircuitBreaker"; + } + + public static class Config implements HasRouteId { + + private String name; + + private URI fallbackUri; + + private String routeId; + + @Override + public void setRouteId(String routeId) { + this.routeId = routeId; + } + + public String getRouteId() { + return routeId; + } + + public URI getFallbackUri() { + return fallbackUri; + } + + public Config setFallbackUri(URI fallbackUri) { + this.fallbackUri = fallbackUri; + return this; + } + + public Config setFallbackUri(String fallbackUri) { + return setFallbackUri(URI.create(fallbackUri)); + } + + public String getName() { + return name; + } + + public Config setName(String name) { + this.name = name; + return this; + } + + public String getId() { + if (StringUtils.isEmpty(name) && !StringUtils.isEmpty(routeId)) { + return routeId; + } + return name; + } + + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerHystrixFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerHystrixFilterFactory.java new file mode 100644 index 000000000..fd026ed85 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerHystrixFilterFactory.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory; + +import com.netflix.hystrix.exception.HystrixRuntimeException; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; +import org.springframework.cloud.gateway.support.ServiceUnavailableException; +import org.springframework.cloud.gateway.support.TimeoutException; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.server.ResponseStatusException; + +/** + * @author Ryan Baxter + */ +public class SpringCloudCircuitBreakerHystrixFilterFactory + extends SpringCloudCircuitBreakerFilterFactory { + + public SpringCloudCircuitBreakerHystrixFilterFactory( + ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, + ObjectProvider dispatcherHandlerProvider) { + super(reactiveCircuitBreakerFactory, dispatcherHandlerProvider); + } + + @Override + protected Mono handleErrorWithoutFallback(Throwable throwable) { + if (throwable instanceof HystrixRuntimeException) { + HystrixRuntimeException e = (HystrixRuntimeException) throwable; + HystrixRuntimeException.FailureType failureType = e.getFailureType(); + + switch (failureType) { + case TIMEOUT: + return Mono.error(new TimeoutException()); + case SHORTCIRCUIT: + return Mono.error(new ServiceUnavailableException()); + case COMMAND_EXCEPTION: { + Throwable cause = e.getCause(); + + /* + * We forsake here the null check for cause as HystrixRuntimeException + * will always have a cause if the failure type is COMMAND_EXCEPTION. + */ + if (cause instanceof ResponseStatusException + || AnnotatedElementUtils.findMergedAnnotation(cause.getClass(), + ResponseStatus.class) != null) { + return Mono.error(cause); + } + } + default: + break; + } + } + return Mono.error(throwable); + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerResilience4JFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerResilience4JFilterFactory.java new file mode 100644 index 000000000..ddda8f99b --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerResilience4JFilterFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory; + +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; +import org.springframework.cloud.gateway.support.ServiceUnavailableException; +import org.springframework.cloud.gateway.support.TimeoutException; +import org.springframework.web.reactive.DispatcherHandler; + +/** + * @author Ryan Baxter + */ +public class SpringCloudCircuitBreakerResilience4JFilterFactory + extends SpringCloudCircuitBreakerFilterFactory { + + public SpringCloudCircuitBreakerResilience4JFilterFactory( + ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, + ObjectProvider dispatcherHandlerProvider) { + super(reactiveCircuitBreakerFactory, dispatcherHandlerProvider); + } + + @Override + protected Mono handleErrorWithoutFallback(Throwable t) { + if (java.util.concurrent.TimeoutException.class.isInstance(t)) { + return Mono.error(new TimeoutException()); + } + if (CallNotPermittedException.class.isInstance(t)) { + return Mono.error(new ServiceUnavailableException()); + } + return Mono.error(t); + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index d76e628da..ff2b1e857 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -65,6 +65,7 @@ import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFact import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory; import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory; @@ -73,6 +74,7 @@ import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter; import org.springframework.cloud.gateway.route.Route; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; +import org.springframework.util.StringUtils; import org.springframework.util.unit.DataSize; import org.springframework.web.server.ServerWebExchange; @@ -218,6 +220,20 @@ public class GatewayFilterSpec extends UriSpec { return filter(factory.apply(this.routeBuilder.getId(), configConsumer)); } + public GatewayFilterSpec circuitBreaker( + Consumer configConsumer) { + SpringCloudCircuitBreakerFilterFactory filterFactory; + try { + filterFactory = getBean(SpringCloudCircuitBreakerFilterFactory.class); + } + catch (NoSuchBeanDefinitionException e) { + throw new NoSuchBeanDefinitionException( + SpringCloudCircuitBreakerFilterFactory.class, + "There needs to be a circuit breaker implementation on the classpath that supports reactive APIs."); + } + return filter(filterFactory.apply(this.routeBuilder.getId(), configConsumer)); + } + /** * Maps headers from one name to another. * @param fromHeader the header name of the original header. 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 f41142301..f293ece2e 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 @@ -128,6 +128,12 @@ public final class ServerWebExchangeUtils { public static final String HYSTRIX_EXECUTION_EXCEPTION_ATTR = qualify( "hystrixExecutionException"); + /** + * CircuitBreaker execution exception attribute name. + */ + public static final String CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR = qualify( + "circuitBreakerExecutionException"); + /** * Used when a routing filter has been successfully called. Allows users to write * custom routing filters that disable built in routing filters. diff --git a/spring-cloud-gateway-core/src/main/resources/META-INF/spring.factories b/spring-cloud-gateway-core/src/main/resources/META-INF/spring.factories index eca8b6986..8e1e84c8f 100644 --- a/spring-cloud-gateway-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-gateway-core/src/main/resources/META-INF/spring.factories @@ -2,6 +2,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration,\ org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\ +org.springframework.cloud.gateway.config.GatewayCircuitBreakerAutoConfiguration,\ org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration,\ org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration,\ org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration,\ 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 new file mode 100644 index 000000000..f2966ad78 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerFilterFactoryTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory; + +import org.junit.Test; + +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.springframework.http.MediaType.TEXT_HTML; + +/** + * @author Ryan Baxter + */ +public abstract class SpringCloudCircuitBreakerFilterFactoryTests + extends BaseWebClientTests { + + @Test + public void cbFilterWorks() { + testClient.get().uri("/get").header("Host", "www.sccbsuccess.org").exchange() + .expectStatus().isOk().expectHeader() + .valueEquals(ROUTE_ID_HEADER, "sccb_success_test"); + } + + @Test + public void cbFilterTimesout() { + testClient.get().uri("/delay/3").header("Host", "www.sccbtimeout.org").exchange() + .expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT).expectBody() + .jsonPath("$.status") + .isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value())); + } + + /* + * Tests that timeouts bubbling from the underpinning WebClient are treated the same + * as CircuitBreaker timeouts in terms of outside response. (Internally, timeouts from the + * WebClient are seen as command failures and trigger the opening of circuit breakers + * the same way timeouts do; it may be confusing in terms of the CircuitBreaker metrics + * though) + */ + @Test + public void timeoutFromWebClient() { + testClient.get().uri("/delay/10") + .header("Host", "www.circuitbreakerresponsestall.org").exchange() + .expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT); + } + + @Test + public void filterFallback() { + testClient.get().uri("/delay/3?a=b") + .header("Host", "www.circuitbreakerfallback.org").exchange() + .expectStatus().isOk().expectBody() + .json("{\"from\":\"circuitbreakerfallbackcontroller\"}"); + } + + @Test + public void filterWorksJavaDsl() { + testClient.get().uri("/get").header("Host", "www.circuitbreakerjava.org") + .exchange().expectStatus().isOk().expectHeader() + .valueEquals(ROUTE_ID_HEADER, "circuitbreaker_java"); + } + + @Test + public void filterFallbackJavaDsl() { + testClient.get().uri("/delay/3").header("Host", "www.circuitbreakerjava.org") + .exchange().expectStatus().isOk().expectBody() + .json("{\"from\":\"circuitbreakerfallbackcontroller2\"}"); + } + + @Test + public void filterConnectFailure() { + testClient.get().uri("/delay/3") + .header("Host", "www.circuitbreakerconnectfail.org").exchange() + .expectStatus().is5xxServerError(); + } + + @Test + public void filterErrorPage() { + testClient.get().uri("/delay/3") + .header("Host", "www.circuitbreakerconnectfail.org").accept(TEXT_HTML) + .exchange().expectStatus().is5xxServerError().expectBody() + .consumeWith(res -> { + final String body = new String(res.getResponseBody(), UTF_8); + + Assert.isTrue(body.contains("

Whitelabel Error Page

"), + "Cannot find the expected white-label error page title in the response"); + Assert.isTrue( + body.contains("(type=Internal Server Error, status=500)"), + "Cannot find the expected error status report in the response"); + }); + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerHystrixFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerHystrixFilterFactoryTests.java new file mode 100644 index 000000000..57d85714c --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerHystrixFilterFactoryTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory; + +import com.netflix.config.ConfigurationManager; +import com.netflix.hystrix.Hystrix; +import com.netflix.hystrix.metric.consumer.HealthCountsStream; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.netflix.hystrix.ReactiveHystrixCircuitBreakerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.core.StringContains.containsString; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.cloud.gateway.filter.factory.ExceptionFallbackHandler.RETRIEVED_EXCEPTION; + +/** + * @author Ryan Baxter + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT, properties = { "debug=true", + "spring.cloud.circuitbreaker.resilience4j.enabled=false" }) +@ContextConfiguration(classes = SpringCloudCircuitBreakerTestConfig.class) +@DirtiesContext +public class SpringCloudCircuitBreakerHystrixFilterFactoryTests + extends SpringCloudCircuitBreakerFilterFactoryTests { + + @Test + public void hystrixFilterServiceUnavailable() { + HealthCountsStream.reset(); + Hystrix.reset(); + ConfigurationManager.getConfigInstance() + .setProperty("hystrix.command.failcmd.circuitBreaker.forceOpen", true); + + testClient.get().uri("/delay/3").header("Host", "www.sccbfailure.org").exchange() + .expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); + + HealthCountsStream.reset(); + Hystrix.reset(); + ConfigurationManager.getConfigInstance() + .setProperty("hystrix.command.failcmd.circuitBreaker.forceOpen", false); + } + + @Test + public void hystrixFilterExceptionFallback() { + testClient.get().uri("/delay/3") + .header("Host", "www.circuitbreakerexceptionfallback.org").exchange() + .expectStatus().isOk().expectHeader() + .value(RETRIEVED_EXCEPTION, containsString("HystrixTimeoutException")); + } + + @Test + public void toStringFormat() { + SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config() + .setName("myname").setFallbackUri("forward:/myfallback"); + GatewayFilter filter = new SpringCloudCircuitBreakerHystrixFilterFactory( + new ReactiveHystrixCircuitBreakerFactory(), null).apply(config); + assertThat(filter.toString()).contains("myname").contains("forward:/myfallback"); + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerResilience4JFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerResilience4JFilterFactoryTests.java new file mode 100644 index 000000000..75b36da5b --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerResilience4JFilterFactoryTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; +import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory; +import org.springframework.cloud.client.circuitbreaker.Customizer; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.core.StringContains.containsString; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.cloud.gateway.filter.factory.ExceptionFallbackHandler.RETRIEVED_EXCEPTION; + +/** + * @author Ryan Baxter + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT, properties = { "debug=true", + "spring.cloud.circuitbreaker.hystrix.enabled=false" }) +@ContextConfiguration( + classes = SpringCloudCircuitBreakerResilience4JFilterFactoryTests.Config.class) +@DirtiesContext +public class SpringCloudCircuitBreakerResilience4JFilterFactoryTests + extends SpringCloudCircuitBreakerFilterFactoryTests { + + @Autowired + private Resilience4JCircuitBreakerFactory factory; + + @Test + public void r4jFilterServiceUnavailable() { + testClient.get().uri("/delay/3").header("Host", "www.sccbfailure.org").exchange() + .expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); + } + + @Test + public void r4jFilterExceptionFallback() { + testClient.get().uri("/delay/3") + .header("Host", "www.circuitbreakerexceptionfallback.org").exchange() + .expectStatus().isOk().expectHeader() + .value(RETRIEVED_EXCEPTION, containsString("TimeoutException")); + } + + @Test + public void toStringFormat() { + SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config() + .setName("myname").setFallbackUri("forward:/myfallback"); + GatewayFilter filter = new SpringCloudCircuitBreakerResilience4JFilterFactory( + new ReactiveResilience4JCircuitBreakerFactory(), null).apply(config); + assertThat(filter.toString()).contains("myname").contains("forward:/myfallback"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(BaseWebClientTests.DefaultTestConfig.class) + @RestController + static class Config extends SpringCloudCircuitBreakerTestConfig { + + @Bean + public Customizer slowCusomtizer() { + return factory -> { + factory.addCircuitBreakerCustomizer( + cb -> cb.transitionToForcedOpenState(), "failcmd"); + }; + } + + } + +} 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 new file mode 100644 index 000000000..56c68f681 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SpringCloudCircuitBreakerTestConfig.java @@ -0,0 +1,116 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory; + +import java.util.Collections; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.cloud.netflix.ribbon.RibbonClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +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 static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +/** + * @author Ryan Baxter + */ +@EnableAutoConfiguration +@SpringBootConfiguration +@Import(BaseWebClientTests.DefaultTestConfig.class) +@RestController +@RibbonClient(name = "badservice", configuration = TestBadRibbonConfig.class) +public class SpringCloudCircuitBreakerTestConfig { + + @Value("${test.uri}") + private String uri; + + @RequestMapping("/circuitbreakerFallbackController") + public Map fallbackcontroller(@RequestParam("a") String a) { + return Collections.singletonMap("from", "circuitbreakerfallbackcontroller"); + } + + @RequestMapping("/circuitbreakerFallbackController2") + public Map fallbackcontroller2() { + return Collections.singletonMap("from", "circuitbreakerfallbackcontroller2"); + } + + @Bean + public RouteLocator circuitBreakerRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_java", r -> r.host("**.circuitbreakerjava.org") + .filters(f -> f.prefixPath("/httpbin") + .circuitBreaker(config -> config.setFallbackUri( + "forward:/circuitbreakerFallbackController2"))) + .uri(uri)) + .route("circuitbreaker_connection_failure", r -> r + .host("**.circuitbreakerconnectfail.org") + .filters(f -> f.prefixPath("/httpbin").circuitBreaker(config -> { + })).uri("lb:badservice")) + /* + * This is a route encapsulated in a circuit breaker that is ready to wait + * for a response far longer than the underpinning WebClient would. + */ + .route("circuitbreaker_response_stall", + r -> r.host("**.circuitbreakerresponsestall.org") + .filters(f -> f.prefixPath("/httpbin").circuitBreaker( + config -> config.setName("stalling-command"))) + .uri(uri)) + .build(); + } + + @Bean + CircuitBreakerExceptionFallbackHandler exceptionFallbackHandler() { + return new CircuitBreakerExceptionFallbackHandler(); + } + + @Bean + RouterFunction routerFunction( + CircuitBreakerExceptionFallbackHandler exceptionFallbackHandler) { + return route(GET("/circuitbreakerExceptionFallback"), + exceptionFallbackHandler::retrieveExceptionInfo); + } + +} + +class CircuitBreakerExceptionFallbackHandler { + + static final String RETRIEVED_EXCEPTION = "Retrieved-Exception"; + + Mono retrieveExceptionInfo(ServerRequest serverRequest) { + String exceptionName = serverRequest + .attribute(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR) + .map(exception -> exception.getClass().getName()).orElse(""); + return ServerResponse.ok().header(RETRIEVED_EXCEPTION, exceptionName).build(); + } + +} diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 3e42d8a27..a3d3133cf 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -173,6 +173,52 @@ spring: filters: - Hystrix=successcmd + # ===================================== + - id: sccb_success_test + uri: ${test.uri} + predicates: + - Host=**.sccbsuccess.org + filters: + - CircuitBreaker=successcmd + + # ===================================== + - id: sccb_timeout_test + uri: ${test.uri} + predicates: + - Host=**.sccbtimeout.org + filters: + - CircuitBreaker=timeoutcmd + + # ===================================== + - id: sccb_failure_test + uri: ${test.uri} + predicates: + - Host=**.sccbfailure.org + filters: + - CircuitBreaker=failcmd + + # ===================================== + - id: circuitbreaker_exception_fallback_test + uri: ${test.uri} + predicates: + - Host=**.circuitbreakerexceptionfallback.org + filters: + - name: CircuitBreaker + args: + name: fallbackcmd + fallbackUri: forward:/circuitbreakerExceptionFallback + + # ===================================== + - id: circuitbreaker_fallback_test + uri: ${test.uri} + predicates: + - Host=**.circuitbreakerfallback.org + filters: + - name: CircuitBreaker + args: + name: fallbackcmd + fallbackUri: forward:/circuitbreakerFallbackController + # ===================================== - id: load_balancer_client_test uri: lb://testservice diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 5769f52a2..ba81d4559 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -27,6 +27,7 @@ + \ No newline at end of file