Skip to content

Commit 76e4187

Browse files
author
Ryan Baxter
committed
Initial set of changes to add a circuit breaker filter using Spring Cloud CircuitBreaker
1 parent 3f0364d commit 76e4187

18 files changed

+1017
-26
lines changed

docs/src/main/asciidoc/spring-cloud-gateway.adoc

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,11 @@ The DedupeResponseHeader filter also accepts an optional `strategy` parameter. T
465465

466466
[[hystrix]]
467467
=== Hystrix GatewayFilter Factory
468+
469+
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
470+
Gateway Filter>> with Resilience4J as support for Hystrix will be removed in a future release.
471+
472+
468473
https://github.com/Netflix/Hystrix[Hystrix] is a library from Netflix that implements the https://martinfowler.com/bliki/CircuitBreaker.html[circuit breaker pattern].
469474
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.
470475

@@ -554,10 +559,96 @@ To set a 5 second timeout for the example route above, the following configurati
554559
[source,yaml]
555560
hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 5000
556561

562+
[[spring-cloud-circuitbreaker-filter-factory]]
563+
=== Spring Cloud CircuitBreaker GatewayFilter Factory
564+
565+
The Spring Cloud CircuitBreaker filter factory leverages the Spring Cloud CircuitBreaker APIs to wrap Gateway routes in
566+
a circuit breaker. Spring Cloud CircuitBreaker supports two libraries which can be used with Spring Cloud Gateway, Hystrix
567+
and Resilience4J. Since Netflix has places Hystrix in maintenance only mode we suggest you use Resilience4J.
568+
569+
To enable the Spring Cloud CircuitBreaker filter you will need to either place `spring-cloud-starter-circuitbreaker-reactor-resilience4j` or
570+
`spring-cloud-starter-netflix-hystrix` on the classpath.
571+
572+
.application.yml
573+
[source,yaml]
574+
----
575+
spring:
576+
cloud:
577+
gateway:
578+
routes:
579+
- id: circuitbreaker_route
580+
uri: https://example.org
581+
filters:
582+
- CircuitBreaker=myCircuitBreaker
583+
----
584+
To configure the circuit breaker, see the configuration for the underlying circuit breaker implementation you are using.
585+
586+
* https://cloud.spring.io/spring-cloud-circuitbreaker/reference/html/spring-cloud-circuitbreaker.html[Resilience4J Documentation]
587+
* https://cloud.spring.io/spring-cloud-netflix/reference/html/[Hystrix Documentation]
588+
589+
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.
590+
591+
592+
.application.yml
593+
[source,yaml]
594+
----
595+
spring:
596+
cloud:
597+
gateway:
598+
routes:
599+
- id: circuitbreaker_route
600+
uri: lb://backing-service:8088
601+
predicates:
602+
- Path=/consumingserviceendpoint
603+
filters:
604+
- name: CircuitBreaker
605+
args:
606+
name: myCircuitBreaker
607+
fallbackUri: forward:/incaseoffailureusethis
608+
- RewritePath=/consumingserviceendpoint, /backingserviceendpoint
609+
----
610+
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.
611+
612+
The primary scenario is to use the `fallbackUri` to an internal controller or handler within the gateway app.
613+
However, it is also possible to reroute the request to a controller or handler in an external application, like so:
614+
615+
.application.yml
616+
[source,yaml]
617+
----
618+
spring:
619+
cloud:
620+
gateway:
621+
routes:
622+
- id: ingredients
623+
uri: lb://ingredients
624+
predicates:
625+
- Path=//ingredients/**
626+
filters:
627+
- name: CircuitBreaker
628+
args:
629+
name: fetchIngredients
630+
fallbackUri: forward:/fallback
631+
- id: ingredients-fallback
632+
uri: http://localhost:9994
633+
predicates:
634+
- Path=/fallback
635+
----
636+
637+
In this example, there is no `fallback` endpoint or handler in the gateway application, however, there is one in another
638+
app, registered under `http://localhost:9994`.
639+
640+
In case of the request being forwarded to fallback, the Spring Cloud CircuitBreaker Gateway filter also provides the `Throwable` that has
641+
caused it. It's added to the `ServerWebExchange` as the
642+
`ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR` attribute that can be used when
643+
handling the fallback within the gateway app.
644+
645+
For the external controller/handler scenario, headers can be added with exception details. You can find more information
646+
on it in the <<fallback-headers, FallbackHeaders GatewayFilter Factory section>>.
647+
557648
[[fallback-headers]]
558649
=== FallbackHeaders GatewayFilter Factory
559650

560-
The `FallbackHeaders` factory allows you to add Hystrix execution exception details in headers of a request forwarded to
651+
The `FallbackHeaders` factory allows you to add Hystrix or Spring Cloud CircuitBreaker execution exception details in headers of a request forwarded to
561652
a `fallbackUri` in an external application, like in the following scenario:
562653

563654
.application.yml
@@ -572,7 +663,7 @@ spring:
572663
predicates:
573664
- Path=//ingredients/**
574665
filters:
575-
- name: Hystrix
666+
- name: CircuitBreaker
576667
args:
577668
name: fetchIngredients
578669
fallbackUri: forward:/fallback
@@ -586,7 +677,7 @@ spring:
586677
executionExceptionTypeHeaderName: Test-Header
587678
----
588679

589-
In this example, after an execution exception occurs while running the `HystrixCommand`, the request will be forwarded to
680+
In this example, after an execution exception occurs while running the circuit breaker, the request will be forwarded to
590681
the `fallback` endpoint or handler in an app running on `localhost:9994`. The headers with the exception type, message
591682
and -if available- root cause exception type and message will be added to that request by the `FallbackHeaders` filter.
592683

@@ -598,7 +689,8 @@ their default values:
598689
* `rootCauseExceptionTypeHeaderName` (`"Root-Cause-Exception-Type"`)
599690
* `rootCauseExceptionMessageHeaderName` (`"Root-Cause-Exception-Message"`)
600691

601-
You can find more information on how Hystrix works with Gateway in the <<hystrix, Hystrix GatewayFilter Factory section>>.
692+
For more information of circuit beakers and the Gateway see the <<hystrix, Hystrix GatewayFilter Factory section>> or
693+
<<spring-cloud-circuitbreaker-filter-factory, Spring Cloud CircuitBreaker Factory section>>.
602694

603695
=== MapRequestHeader GatewayFilter Factory
604696
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.

pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<java.version>1.8</java.version>
5555
<spring-cloud-commons.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-commons.version>
5656
<spring-cloud-netflix.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-netflix.version>
57+
<spring-cloud-circuitbreaker.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-circuitbreaker.version>
5758
<embedded-redis.version>0.6</embedded-redis.version>
5859
</properties>
5960

@@ -111,6 +112,18 @@
111112
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
112113
<version>${spring-cloud-netflix.version}</version>
113114
</dependency>
115+
<dependency>
116+
<groupId>org.springframework.cloud</groupId>
117+
<artifactId>spring-cloud-circuitbreaker-dependencies</artifactId>
118+
<version>${spring-cloud-circuitbreaker.version}</version>
119+
<type>pom</type>
120+
<scope>import</scope>
121+
</dependency>
122+
<dependency>
123+
<groupId>org.springframework.cloud</groupId>
124+
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
125+
<version>${spring-cloud-circuitbreaker.version}</version>
126+
</dependency>
114127
<dependency>
115128
<groupId>org.springframework.boot</groupId>
116129
<artifactId>spring-boot-devtools</artifactId>

spring-cloud-gateway-core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@
8787
<artifactId>spring-boot-autoconfigure-processor</artifactId>
8888
<optional>true</optional>
8989
</dependency>
90+
<dependency>
91+
<groupId>org.springframework.cloud</groupId>
92+
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
93+
<optional>true</optional>
94+
</dependency>
9095
<dependency>
9196
<groupId>org.springframework.cloud</groupId>
9297
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,7 @@ public HystrixGatewayFilterFactory hystrixGatewayFilterFactory(
710710
}
711711

712712
@Bean
713+
@ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class)
713714
public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() {
714715
return new FallbackHeadersGatewayFilterFactory();
715716
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2013-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.config;
18+
19+
import org.springframework.beans.factory.ObjectProvider;
20+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
21+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
25+
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JAutoConfiguration;
26+
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
27+
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
28+
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
29+
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerHystrixFilterFactory;
30+
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerResilience4JFilterFactory;
31+
import org.springframework.cloud.netflix.hystrix.HystrixCircuitBreakerAutoConfiguration;
32+
import org.springframework.cloud.netflix.hystrix.ReactiveHystrixCircuitBreakerFactory;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.web.reactive.DispatcherHandler;
36+
37+
/**
38+
* @author Ryan Baxter
39+
*/
40+
@Configuration
41+
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
42+
@AutoConfigureAfter({ ReactiveResilience4JAutoConfiguration.class,
43+
HystrixCircuitBreakerAutoConfiguration.class })
44+
@ConditionalOnClass({ DispatcherHandler.class,
45+
ReactiveResilience4JAutoConfiguration.class,
46+
HystrixCircuitBreakerAutoConfiguration.class })
47+
public class GatewayCircuitBreakerAutoConfiguration {
48+
49+
@Configuration
50+
@ConditionalOnClass({ ReactiveCircuitBreakerFactory.class,
51+
ReactiveHystrixCircuitBreakerFactory.class })
52+
protected static class SpringCloudCircuitBreakerHystrixConfiguration {
53+
54+
@Bean
55+
@ConditionalOnBean(ReactiveHystrixCircuitBreakerFactory.class)
56+
public SpringCloudCircuitBreakerHystrixFilterFactory springCloudCircuitBreakerHystrixFilterFactory(
57+
ReactiveHystrixCircuitBreakerFactory reactiveCircuitBreakerFactory,
58+
ObjectProvider<DispatcherHandler> dispatcherHandler) {
59+
return new SpringCloudCircuitBreakerHystrixFilterFactory(
60+
reactiveCircuitBreakerFactory, dispatcherHandler);
61+
}
62+
63+
@Bean
64+
@ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class)
65+
public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() {
66+
return new FallbackHeadersGatewayFilterFactory();
67+
}
68+
69+
}
70+
71+
@Configuration
72+
@ConditionalOnClass({ ReactiveCircuitBreakerFactory.class,
73+
ReactiveResilience4JCircuitBreakerFactory.class })
74+
protected static class Resilience4JConfiguration {
75+
76+
@Bean
77+
@ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class)
78+
public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() {
79+
return new FallbackHeadersGatewayFilterFactory();
80+
}
81+
82+
@Bean
83+
@ConditionalOnBean(ReactiveResilience4JCircuitBreakerFactory.class)
84+
public SpringCloudCircuitBreakerResilience4JFilterFactory springCloudCircuitBreakerResilience4JFilterFactory(
85+
ReactiveResilience4JCircuitBreakerFactory reactiveCircuitBreakerFactory,
86+
ObjectProvider<DispatcherHandler> dispatcherHandler) {
87+
return new SpringCloudCircuitBreakerResilience4JFilterFactory(
88+
reactiveCircuitBreakerFactory, dispatcherHandler);
89+
}
90+
91+
}
92+
93+
}

spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/FallbackHeadersGatewayFilterFactory.java

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
import static java.util.Collections.singletonList;
2626
import static java.util.Optional.ofNullable;
2727
import static org.apache.commons.lang.exception.ExceptionUtils.getRootCause;
28+
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR;
2829
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR;
2930

3031
/**
3132
* @author Olga Maciaszek-Sharma
33+
* @author Ryan Baxter
3234
*/
3335
public class FallbackHeadersGatewayFilterFactory
3436
extends AbstractGatewayFilterFactory<FallbackHeadersGatewayFilterFactory.Config> {
@@ -45,29 +47,37 @@ public List<String> shortcutFieldOrder() {
4547
@Override
4648
public GatewayFilter apply(Config config) {
4749
return (exchange, chain) -> {
48-
ServerWebExchange filteredExchange = ofNullable(
50+
ServerWebExchange filteredExchange = ofNullable(ofNullable(
4951
(Throwable) exchange.getAttribute(HYSTRIX_EXECUTION_EXCEPTION_ATTR))
50-
.map(executionException -> {
51-
ServerHttpRequest.Builder requestBuilder = exchange
52-
.getRequest().mutate();
53-
requestBuilder.header(
54-
config.executionExceptionTypeHeaderName,
55-
executionException.getClass().getName());
56-
requestBuilder.header(
57-
config.executionExceptionMessageHeaderName,
58-
executionException.getMessage());
59-
ofNullable(getRootCause(executionException))
60-
.ifPresent(rootCause -> {
61-
requestBuilder.header(
62-
config.rootCauseExceptionTypeHeaderName,
63-
rootCause.getClass().getName());
64-
requestBuilder.header(
65-
config.rootCauseExceptionMessageHeaderName,
66-
rootCause.getMessage());
67-
});
68-
return exchange.mutate().request(requestBuilder.build())
69-
.build();
70-
}).orElse(exchange);
52+
.orElseGet(() -> exchange.getAttribute(
53+
CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR)))
54+
.map(executionException -> {
55+
ServerHttpRequest.Builder requestBuilder = exchange
56+
.getRequest().mutate();
57+
requestBuilder.header(
58+
config.executionExceptionTypeHeaderName,
59+
executionException.getClass()
60+
.getName());
61+
requestBuilder.header(
62+
config.executionExceptionMessageHeaderName,
63+
executionException.getMessage());
64+
ofNullable(
65+
getRootCause(executionException))
66+
.ifPresent(rootCause -> {
67+
requestBuilder.header(
68+
config.rootCauseExceptionTypeHeaderName,
69+
rootCause
70+
.getClass()
71+
.getName());
72+
requestBuilder.header(
73+
config.rootCauseExceptionMessageHeaderName,
74+
rootCause
75+
.getMessage());
76+
});
77+
return exchange.mutate()
78+
.request(requestBuilder.build())
79+
.build();
80+
}).orElse(exchange);
7181
return chain.filter(filteredExchange);
7282
};
7383
}

0 commit comments

Comments
 (0)