In part 2 of that post , we will follow up the following :
- How to externally configure resilience4j time limiter with the target circuit breaker
- How to check HTTP status code through spring cloud gateway filter to trigger the related configured circuit breaker

In that post we will show the case of how you can mix the usage of the Resilience4j spring boot starter and spring cloud circuit breaker starter so you can configure externally through spring configuration your circuit breakers definitions if you do not want to use the code configuration approach provided by Spring cloud circuit breaker starter through Customizers.
The whole code sample is in github .
How to enable Resilience4j time limiter with related circuit breaker in Spring cloud Gateway:
You need to configure your resilience4j time limter with same instance name of your circuit breaker in your spring external application yaml file
resilience4j.timelimiter:
time-limiter-aspect-order: 398
configs:
default:
timeoutDuration: 1s
cancelRunningFuture: false
instances:
backendB:
timeoutDuration: 250ms
Then you need to follow the spring cloud circuit breaker factory customization we did in part 1 by extending it to pass the Time limiter configuration from the spring managed Time limiter registery :
spring:
application:
name: gateway-service
output.ansi.enabled: ALWAYS
cloud:
gateway:
routes:
- id: test-service-withResilient4j
uri: http://localhost:8091
predicates:
- Path=/testService/**
filters:
- RewritePath=/testService/(?<path>.*), /${path}
and that is it , now your managed spring cloud circuit breaker route instance backendB use also your configured Time limiter configuration
How we check HTTP status code of the response to trigger circuit breaker for specific one if needed :
Now suppose you want to enanble circuit breaker for specific http status code for exmaple in our code 500 , what you need to do is the following :
- Create spring cloud gate way filter that that check HTTP response code and if it is 500 , you can through HTTP ResposneStatusException
@Component
public class HttpStatusCodeFilter extends AbstractGatewayFilterFactory<HttpStatusCodeFilter.Config> {
@Override
public String name() {
return "StatusCodeCheck";
}
public HttpStatusCodeFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> chain.filter(exchange).then(
Mono.defer(() -> {
if (!exchange.getResponse().isCommitted() &&
HttpStatus.INTERNAL_SERVER_ERROR.equals(exchange.getResponse().getStatusCode())) {
return Mono.error(new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR));
}
return Mono.empty();
}));
}
public static class Config {
// Put the configuration properties
}
}
- Then define a resilience4j circuit breaker record exception predicate to check if it is 500 or not which will mark the call as a failure and trigger your circuit breaker state management logic for that service call
public class HttpInternalServicePredicate implements Predicate<ResponseStatusException> {
@Override
public boolean test(ResponseStatusException e) {
return e.getStatus().is5xxServerError();
}
}
- update your external circuit breaker configuration to reference your predicate
instances:
backendA:
baseConfig: default
backendB:
slidingWindowSize: 10
minimumNumberOfCalls: 10
permittedNumberOfCallsInHalfOpenState: 3
waitDurationInOpenState: 1s
failureRateThreshold: 50
eventConsumerBufferSize: 10
recordFailurePredicate: io.github.romeh.services.gateway.HttpInternalServicePredicate
- Now we will update the test we have to test the the external service call that has the 2 filters enabled (status check and circuit breaker )
static class Initializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.cloud.gateway.routes[0].id=test-service-withResilient4j",
"spring.cloud.gateway.routes[0].uri=" + mockServerContainer.getEndpoint(),
"spring.cloud.gateway.routes[0].predicates[0]=" + "Path=/testService/**",
"spring.cloud.gateway.routes[0].filters[0]=" + "RewritePath=/testService/(?<path>.*), /$\\{path}",
"spring.cloud.gateway.routes[0].filters[1].name=" + "CircuitBreaker",
"spring.cloud.gateway.routes[0].filters[1].args.name=" + "backendA",
"spring.cloud.gateway.routes[0].filters[1].args.fallbackUri=" + "forward:/fallback/testService",
"spring.cloud.gateway.routes[1].id=test-service-withResilient4j-statusCode",
"spring.cloud.gateway.routes[1].uri=" + mockServerContainer.getEndpoint(),
"spring.cloud.gateway.routes[1].predicates[0]=" + "Path=/testInternalServiceError/**",
"spring.cloud.gateway.routes[1].filters[0]=" + "RewritePath=/testInternalServiceError/(?<path>.*), /$\\{path}",
"spring.cloud.gateway.routes[1].filters[1].name=" + "CircuitBreaker",
"spring.cloud.gateway.routes[1].filters[1].args.name=" + "backendB",
"spring.cloud.gateway.routes[1].filters[1].args.fallbackUri=" + "forward:/fallback/testInternalServiceError",
"spring.cloud.gateway.routes[1].filters[2]=StatusCodeCheck"
).applyTo(configurableApplicationContext.getEnvironment());
}
}
- Trigger the test case and you should see in the console the reported events for call failures in your circuit breaker for instance backendB
17:34:29.643 --- [pool-2-thread-1] : 1. Received: status->200, payload->FallbackResponse(msgCode=1, msg=1000000), call->1
17:34:29.664 --- [ctor-http-nio-3] : CircuitBreaker 'backendB' recorded an exception as failure:
org.springframework.web.server.ResponseStatusException: 500 INTERNAL_SERVER_ERROR
at io.github.romeh.services.gateway.HttpStatusCodeFilter.lambda$null$0(HttpStatusCodeFilter.java:32) ~[classes/:na]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.ignoreDone(MonoIgnoreThen.java:190) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreInner.onComplete(MonoIgnoreThen.java:240) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.Operators$MonoSubscriber.onComplete(Operators.java:1797) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onComplete(MonoIgnoreThen.java:314) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.Operators.complete(Operators.java:135) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoEmpty.subscribe(MonoEmpty.java:45) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.ignoreDone(MonoIgnoreThen.java:190) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreInner.onComplete(MonoIgnoreThen.java:240) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2319) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoFlatMapMany$FlatMapManyMain.onSubscribeInner(MonoFlatMapMany.java:143) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoFlatMapMany$FlatMapManyMain.onNext(MonoFlatMapMany.java:182) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.FluxRetryPredicate$RetryPredicateSubscriber.onNext(FluxRetryPredicate.java:82) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.core.publisher.MonoCreate$DefaultMonoSink.success(MonoCreate.java:156) [reactor-core-3.3.5.RELEASE.jar:3.3.5.RELEASE]
at reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver.onStateChange(HttpClientConnect.java:428) [reactor-netty-0.9.7.RELEASE.jar:0.9.7.RELEASE]
at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:514) [reactor-netty-0.9.7.RELEASE.jar:0.9.7.RELEASE]
at reactor.netty.resources.PooledConnectionProvider$DisposableAcquire.onStateChange(PooledConnectionProvider.java:536) [reactor-netty-0.9.7.RELEASE.jar:0.9.7.RELEASE]
at reactor.netty.resources.PooledConnectionProvider$PooledConnection.onStateChange(PooledConnectionProvider.java:427) [reactor-netty-0.9.7.RELEASE.jar:0.9.7.RELEASE]
at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:562) [reactor-netty-0.9.7.RELEASE.jar:0.9.7.RELEASE]
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:96) [reactor-netty-0.9.7.RELEASE.jar:0.9.7.RELEASE]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324) [netty-codec-4.1.49.Final.jar:4.1.49.Final]
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296) [netty-codec-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493) [netty-transport-4.1.49.Final.jar:4.1.49.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989) [netty-common-4.1.49.Final.jar:4.1.49.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [netty-common-4.1.49.Final.jar:4.1.49.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-common-4.1.49.Final.jar:4.1.49.Final]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_152]
17:34:29.665 --- [ctor-http-nio-3] : Event ERROR published: 2020-05-25T17:34:29.665+02:00[Europe/Brussels]: CircuitBreaker 'backendB' recorded an error: 'org.springframework.web.server.ResponseStatusException: 500 INTERNAL_SERVER_ERROR'. Elapsed time: 10 ms
References:
- Resilienc4j :https://resilience4j.readme.io/docs
- Spring cloud circuit breaker :https://spring.io/projects/spring-cloud-circuitbreaker
- Spring Cloud gateway :https://cloud.spring.io/spring-cloud-gateway/reference/html/