Skip to content

Commit

Permalink
Initial set of changes to add a circuit breaker filter using Spring C…
Browse files Browse the repository at this point in the history
…loud CircuitBreaker
  • Loading branch information
Ryan Baxter committed Nov 5, 2019
1 parent 3f0364d commit 76e4187
Show file tree
Hide file tree
Showing 18 changed files with 1,017 additions and 26 deletions.
100 changes: 96 additions & 4 deletions docs/src/main/asciidoc/spring-cloud-gateway.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<spring-cloud-circuitbreaker-filter-factory, Spring Cloud CircuitBreaker
Gateway Filter>> 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.

Expand Down Expand Up @@ -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 section>>.

[[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
Expand All @@ -572,7 +663,7 @@ spring:
predicates:
- Path=//ingredients/**
filters:
- name: Hystrix
- name: CircuitBreaker
args:
name: fetchIngredients
fallbackUri: forward:/fallback
Expand All @@ -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.

Expand All @@ -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 <<hystrix, Hystrix GatewayFilter Factory section>>.
For more information of circuit beakers and the Gateway see the <<hystrix, Hystrix GatewayFilter Factory section>> or
<<spring-cloud-circuitbreaker-filter-factory, Spring Cloud CircuitBreaker Factory section>>.

=== 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.
Expand Down
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<java.version>1.8</java.version>
<spring-cloud-commons.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-netflix.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-netflix.version>
<spring-cloud-circuitbreaker.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-circuitbreaker.version>
<embedded-redis.version>0.6</embedded-redis.version>
</properties>

Expand Down Expand Up @@ -111,6 +112,18 @@
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>${spring-cloud-netflix.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-circuitbreaker-dependencies</artifactId>
<version>${spring-cloud-circuitbreaker.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
<version>${spring-cloud-circuitbreaker.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
Expand Down
5 changes: 5 additions & 0 deletions spring-cloud-gateway-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ public HystrixGatewayFilterFactory hystrixGatewayFilterFactory(
}

@Bean
@ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class)
public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() {
return new FallbackHeadersGatewayFilterFactory();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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> 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> dispatcherHandler) {
return new SpringCloudCircuitBreakerResilience4JFilterFactory(
reactiveCircuitBreakerFactory, dispatcherHandler);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
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<FallbackHeadersGatewayFilterFactory.Config> {
Expand All @@ -45,29 +47,37 @@ public List<String> shortcutFieldOrder() {
@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);
};
}
Expand Down
Loading

0 comments on commit 76e4187

Please sign in to comment.