Spring Resiliency API

👉 Learn Spring Cloud concepts.


11. Using Resilience4j - retry

Project ref: b9-curcuit-breacker

  • Purpose / Feature
    • Resilience4j is a replacement of netflix-hystrix for circuit breaker framework.
    • Resilience4j is a lightweight fault tolerance library designed for functional programming.
    • Resilience4j provides higher-order functions (decorators) to enhance any functional interface, lambda expression or method reference with a Circuit Breaker, Rate Limiter, Retry or Bulkhead.
    • *NOTE: ** *Resilience4j 1 requires Java 8, while Resilience4j 2 requires Java 17.
  • Steps
    • Project setup: Create a new springboot project with below dependencies.
    • Step-1: Add following dependencies i n POM.xml.
      • org.springframework.boot:spring-boot-starter-actuator
      • org.springframework.boot:spring-boot-starter-web
      • org.springframework.boot:spring-boot-starter-aop
      • io.github.resilience4j:resilience4j-spring-boot2:2.2.0
    • Step-2: Annotate the controller API method with @Retry(name = "default").
      • This enables reties for the API with default configuration with max attempt 3.
    • Step-3: We can provide a custom name and provide customized config for reties.
      • Annotate the controller API method with @Retry(name = "b9-cb-retries").
    • Step-4: Add below properties in application.properties to control max retries.
      • resilience4j.retry.instances.b9-cb-retries.maxAttempts=5#NEW
      • #resilience4j.retry.instances.b9-cb-retries.maxRetryAttempts=5#OLD
    • Step-5: Add below properties in application.properties to wait duration between each retries, in this example - 1 seconds.
      • resilience4j.retry.instances.b9-cb-retries.wait-duration=1s
    • Step-6: Increase the wait duration exponentionally between each reties.
      • So first retry after 1s, next 2s, next 4s, next 8s and so on.
      • resilience4j.retry.instances.b9-cb-retries.enable-exponential-backoff=true
    • Step-7: Configure fallbackMathod property to return hard-coded response if failed after reties.
      • @Retry(name = "default", fallbackMethod="hardcodedResponse")
      • Define a method names hardcodedResponse with method argument accepting Throwable intance.
      • We can overload this method with different Exception types, to process fallback for different APIs.
  • Maven / External dependency
    • Required dependency. ```xml org.springframework.bootspring-boot-starter-actuatororg.springframework.bootspring-boot-starter-web

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      
      <dependency>
          <groupId>io.github.resilience4j</groupId>
          <artifactId>resilience4j-spring-boot2</artifactId>
          <version>2.2.0</version>
      </dependency>
      
  • Code / Config changes
    • Controller:HelloWorldController.java
      • imports
        • import io.github.resilience4j.retry.annotation.Retry;
      • Annotate the method with @Retry annotation.
          @RestController
          public class HelloWorldController {
        
              private static final Logger logger = LoggerFactory.getLogger(HelloWorldController.class);
              private Integer counter = 1;
        
              /**
              * Retry default return failure after 3 retries.
              * @return
              */
              @GetMapping("/greet")
              @Retry(name = "default", fallbackMethod = "hardcodedResponseFallbackMethod")
              public String greeting() {
        
                  logger.info("***** HelloWorldController.greeting() method called.");
                  logger.info("***** Request id : {}", counter);
                  try {
                      if (counter % 5 == 0) {
                          throw new DefaultRetryRuntimeException("curcuite breacker test. Request Id : " + counter);
                      }
                  } catch (Exception ex) {
                      logger.info("**** Failed for request id : {}", counter);
                      throw ex;
                  } finally {
                      counter++;
                  }
        
                  return "Guten Morgen";
              }
        
              /**
              * Custom retry as configured in the application.properties.
              * @return
              */
              @GetMapping("/greet-cr")
              @Retry(name = "b9-cb-retries", fallbackMethod = "hardcodedResponseFallbackMethod")
              public String greetingCustomRetries() {
        
                  logger.info("***** HelloWorldController.greeting() method called.");
                  logger.info("***** Request id : {}", counter);
                  try {
                      if (counter % 6 != 0) {
                          throw new CustomRetryRuntimeException("curcuite breacker test. Request Id : " + counter);
                      }
                  } catch (Exception ex) {
                      logger.info("**** Failed for request id : {}", counter);
                      throw ex;
                  } finally {
                      counter++;
                  }
        
                  return "Guten Morgen";
              }
        
              /**
              * Circuit breaker fallback method.
              * @param ex
              */
              public String hardcodedResponseFallbackMethod(Exception ex) {
        
                  if (ex instanceof DefaultRetryRuntimeException) {
                      return "Guten Morgen, default resonse for DefaultRetryRuntimeException";
                  } else if (ex instanceof CustomRetryRuntimeException) {
                      return "Guten Morgen, default resonse for CustomRetryRuntimeException";
                  } else {
                      return "default response.";
                  }
              }
          }
        
    • Application Config:application.properties
          spring.application.name=b9-curcuit-breacker
      
          # Start: Circuit breaker config
      
          # custom Retry - Max Retries for 5
          resilience4j.retry.instances.b9-cb-retries.max-attempts=5
      
          # Wait duration between each retries
          resilience4j.retry.instances.b9-cb-retries.wait-duration=1s
      
          # Increase the wait duration exponentionally between each reties
          resilience4j.retry.instances.b9-cb-retries.enable-exponential-backoff=true
      
      
          # End: Circuit breaker config
      

Note: This is an important note.


12. Using Resilience4j - circuit breaker

Project ref: b9-curcuit-breacker

  • Purpose / Feature
    • Note: This project is extention to Using Resilience4j - retry. So all settings will remain same except for that instead of @Retry** annotation we’ll use **@CircuitBreaker* annotation.
    • If there are continious failure identified for a microservice, then after certain attemts, circuit breaker stop processing the API and send the response directly from fallbackMethod.
    • The CircuitBreaker is implemented via a finite state machine with three normal states: CLOSED, OPEN and HALF_OPEN and two special states DISABLED and FORCED_OPEN.
    • It uses a sliding window to store and aggregate the outcome of calls. You can choose between a count-based sliding window and a time-based sliding window.
      • The count-based sliding window aggregrates the outcome of the last N calls.
      • The time-based sliding window aggregrates the outcome of the calls of the last N seconds.
    • Aspect order: The Resilience4j Aspects order is the following:
      • Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) )
  • Steps
    • Project setup: Follow Section 11. Using Resilience4j - retry for project setup.
    • Step-1: Replace @Retry annotation with @CircuitBreaker annotation.
      • @CircuitBreaker(name = "default", fallbackMethod = "hardcodedResponseFallbackMethod")
    • Step-2: keep other settings as it.
    • Step-3: Configure to increase retry duration exponentilly in case all requests keep failing.
      • resilience4j.retry.instances.b9-cb-retries.enable-exponential-backoff=true
  • Maven / External dependency
    • Required dependency. ```xml org.springframework.bootspring-boot-starter-actuatororg.springframework.bootspring-boot-starter-web

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      
      <dependency>
          <groupId>io.github.resilience4j</groupId>
          <artifactId>resilience4j-spring-boot2</artifactId>
          <version>2.2.0</version>
      </dependency>
      
  • Code / Config changes
    • Controller:HelloWorldControllerCircuitBreaker.java
      • imports
        • import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
      • Annotate the method with @CircuitBreaker annotation.
          @RestController
          public class HelloWorldControllerCircuitBreaker {
        
              private static final Logger logger = LoggerFactory.getLogger(HelloWorldControllerCircuitBreaker.class);
              private Integer counter = 1;
        
              /**
              * Retry default return failure after 3 retries.
              * @return
              */
              @GetMapping("/cb/greet")
              // @Retry(name = "default", fallbackMethod = "hardcodedResponseFallbackMethod")
              @CircuitBreaker(name = "default", fallbackMethod = "hardcodedResponseFallbackMethod")
              public String greeting() {
        
                  logger.info("***** HelloWorldController.greeting() method called.");
                  logger.info("***** Request id : {}", counter);
                  try {
                      if (counter % 2	 == 0) {
                          throw new DefaultRetryRuntimeException("curcuite breacker test. Request Id : " + counter);
                      }
                  } catch (Exception ex) {
                      logger.info("**** Failed for request id : {}", counter);
                      throw ex;
                  } finally {
                      counter++;
                  }
                  return "Guten Morgen";
              }
        
              /**
              * Circuit breaker fallback method.
              * @param ex
              */
              public String hardcodedResponseFallbackMethod(Exception ex) {
        
                  if (ex instanceof DefaultRetryRuntimeException) {
                      return "Guten Morgen, default resonse for DefaultRetryRuntimeException";
                  } else if (ex instanceof CustomRetryRuntimeException) {
                      return "Guten Morgen, default resonse for CustomRetryRuntimeException";
                  } else {
                      return "default response.";
                  }
              }
          }
        
    • Application Config:application.properties
          spring.application.name=b9-curcuit-breacker
      
          # Start: Circuit breaker config
      
          # custom Retry - Max Retries for 5
          resilience4j.retry.instances.b9-cb-retries.max-attempts=5
      
          # Wait duration between each retries
          resilience4j.retry.instances.b9-cb-retries.wait-duration=1s
      
          # Increase the wait duration exponentionally between each reties
          resilience4j.retry.instances.b9-cb-retries.enable-exponential-backoff=true
      
      
          # End: Circuit breaker config
      

Note: This is an important note.

  • Notes:
    • There’s many more advance configutation for circuit breaked and retry.
      • Follow the link in Reference section.
    • The fallback method mechanism works like a try/catch block. If a fallback method is configured, every exception is forwarded to a fallback method executor.
    • The fallback method executor is searching for the best matching fallback method which can handle the exception. Similar to a catch block.
    • The fallback is executed independently of the current state of the circuit breaker.
    • It’s important to remember that a fallback method should be placed in the same class and must have the same method signature with just ONE extra target exception parameter.
  • References:

13. Using Resilience4j - API advance configuration

Project ref: b9-curcuit-breacker

  • Purpose / Feature
    • Note: Advance API configuration.
    • Aspect order: The Resilience4j Aspects order is the following:
      • Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) )
    • Rate Limiting: Setting up the max allowed API calls to the annotated API method during a time period.
    • Time Limiting: Setting up max API calls allowed in a defined time duration.
    • Bulk Head: Define the max concurrent requests to be processed by the API.
    • We can also enable Health checks circuit breaked and Ratelimiter which are by default disabled.
  • Steps
    • Project setup: Follow Section 11. Using Resilience4j - retry for project setup.
    • Step-1: Add annotation @RateLimiter, to configure the no. of requests allowed in a give time period.
    • Step-2: Add annotation @Bulkhead, to configure the no. of requests allowed for concurrent execution.
    • Step-3: Add annotation @TimeLimiter, for configure allowed execution time.
  • Maven / External dependency
    • Required dependency. ```xml org.springframework.bootspring-boot-starter-actuatororg.springframework.bootspring-boot-starter-web

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      
      <dependency>
          <groupId>io.github.resilience4j</groupId>
          <artifactId>resilience4j-spring-boot2</artifactId>
          <version>2.2.0</version>
      </dependency>
      
  • Code / Config changes
    • Controller:HelloWorldControllerCircuitBreaker.java
      • imports
        • import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
      • Annotate the method with @CircuitBreaker annotation.
          @RestController
          public class HelloWorldControllerCircuitBreaker {
        
              private static final Logger logger = LoggerFactory.getLogger(HelloWorldControllerCircuitBreaker.class);
              private Integer counter = 1;
        
              /**
              * Retry default return failure after 3 retries.
              * 
              * @return
              */
              @GetMapping("/cb/greet")
              @CircuitBreaker(name = "greeting-api", fallbackMethod = "hardcodedResponseFallbackMethod")
              @Retry(name = "greeting-api", fallbackMethod = "hardcodedResponseFallbackMethod")
              public String greeting() {
        
                  logger.info("***** HelloWorldController.greeting() method called.");
                  logger.info("***** Request id : {}", counter);
                  try {
                      if (counter % 2 == 0) {
                          throw new DefaultRetryRuntimeException("curcuite breacker test. Request Id : " + counter);
                      }
                  } catch (Exception ex) {
                      logger.info("**** Failed for request id : {}", counter);
                      throw ex;
                  } finally {
                      counter++;
                  }
        
                  return "Guten Morgen";
              }
        
              /**
              * Testing other annotations.
              * 
              * @return
              */
              @GetMapping("/cb/greet-adv")
              @CircuitBreaker(name = "greeting-api", fallbackMethod = "hardcodedResponseFallbackMethod")
              // we can also add fallbackMethod = "RateLimiterFallbackMethod"
              @RateLimiter(name = "greeting-api")
              // we can also add fallbackMethod = "bulkHeadFallbackMethod"
              @Bulkhead(name = "greeting-api")
              @Retry(name = "greeting-api", fallbackMethod = "hardcodedResponseFallbackMethod")
              // We can also add fallbackMethod = "timeLimiterFallbackMethod"
              @TimeLimiter(name = "greeting-api")
              public String greetingAdvance() {
        
                  logger.info("***** HelloWorldController.greeting() method called.");
                  logger.info("***** Request id : {}", counter);
                  try {
                      if (counter % 2 == 0) {
                          throw new DefaultRetryRuntimeException("curcuite breacker test. Request Id : " + counter);
                      }
                  } catch (Exception ex) {
                      logger.info("**** Failed for request id : {}", counter);
                      throw ex;
                  } finally {
                      counter++;
                  }
        
                  return "Guten Morgen";
              }
        
              /**
              * Circuit breaker fallback method.
              * 
              * @param ex
              */
              public String hardcodedResponseFallbackMethod(Exception ex) {
        
                  if (ex instanceof DefaultRetryRuntimeException) {
                      return "Guten Morgen, default resonse for DefaultRetryRuntimeException";
                  } else if (ex instanceof CustomRetryRuntimeException) {
                      return "Guten Morgen, default resonse for CustomRetryRuntimeException";
                  } else {
                      return "default response.";
                  }
              }
          }
        
    • Application Config:application.properties
          spring.application.name=b9-curcuit-breacker
      
          # Start: Circuit breaker config
      
          # custom Retry - Max Retries for 5
          resilience4j.retry.instances.b9-cb-retries.max-attempts=5
      
          # Wait duration between each retries
          resilience4j.retry.instances.b9-cb-retries.wait-duration=1s
      
          # Increase the wait duration exponentionally between each reties
          resilience4j.retry.instances.b9-cb-retries.enable-exponential-backoff=true
      
          # Start: Enable health checks for actuator
          management.health.circuitbreakers.enabled: true
          management.health.ratelimiters.enabled: true
      
          resilience4j.ratelimiter.instances.default.register-health-indicator=true
          resilience4j.ratelimiter.instances.greeting-api.register-health-indicator=true
          resilience4j.ratelimiter.instances.b9-cb-retries.register-health-indicator=true
      
          # End: Enable health checks for actuator
      
      
          # Ratelimiter - allows max of 2 reuests in every 10 seconds.
          resilience4j.ratelimiter.instances.greeting-api.limit-for-period=2
          resilience4j.ratelimiter.instances.greeting-api.limit-refresh-period=10s
          resilience4j.ratelimiter.instances.greeting-api.timeout-duration=3s
          resilience4j.ratelimiter.instances.greeting-api.event-consumer-buffer-size=100
      
          # BulkHead - Allows maximum of only 10 concurrent calls.
          resilience4j.bulkhead.instances.greeting-api.max-concurrent-calls=10
          resilience4j.thread-pool-bulkhead.instances.greeting-api.core-thread-pool-size=2
          resilience4j.thread-pool-bulkhead.instances.greeting-api.max-thread-pool-size=5
          resilience4j.thread-pool-bulkhead.instances.greeting-api.queue-capacity=1
          resilience4j.thread-pool-bulkhead.instances.greeting-api.writable-stack-trace-enabled=true
          resilience4j.thread-pool-bulkhead.instances.greeting-api.keep-alive-duration=600s
      
      
          # TimeLimier - 
          resilience4j.timelimiter.instances.greeting-api.timeout-duration=2s
          resilience4j.timelimiter.instances.greeting-api.cancel-running-future=true
      
      
          # End: Circuit breaker config
      

Note: This is an important note.