Spring cloud gateway with Resilience4j circuit breaker – Part 2

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:

Leave a Reply