Enforcing Resilience in a Spring Boot App using Resilience4J

An Important Property of Modern Web Apps is Resilience. In simple words, resilience is the ability of a system’s feature to fail gracefully without impacting the entire system. In the case of web apps, we want to make sure that the entire system will not be down if a remote service (a database, API Server) fails (is slow or down).

What is Resilience4J?

Resilience4J is a lightweight, easy-to-use fault tolerance library inspired by Netflix Hystrix but designed for Java 8 and functional programming.

Resilience Modules Provided by Resilience4J

Resilience4J provides a number of modules. Each module represents a resilience pattern and can be implemented independently of each other. For each of the following modules, Resilience4J provides a single dependency. However, when you need to use all of them in your project, you can directly import the starter project provided by Resilience4J. It will contain the dependencies required for all the modules. The image below shows the individual module dependencies on MVN Repository You can refer to the artifact Id of each dependency to differentiate them.

Circuit Breaker

When the number of failures to registered from a call to service is greater than a given threshold, subsequent calls to this service are made unreachable(fail-fast) or a fallback is executed. The finite state machine below describes the functioning of the Circuit Breaker.

Bulkhead

Here, each remote call to a service is provided its own thread pool. This is to ensure that the whole system is not slowed down or frozen if that particular service is slow. A fallback is provided if there is no thread available in the pool to execute a remote call. Semaphores can equally be used to implement this pattern but Thread pools are preferred.

Retry

A max number of attempts is given for a remote call to a service when a failure occurs. A fallback can also be provided in case the call still fails at the last attempt.

Rate Limiter

As its name indicates, this module is used to limit the rate at which a remote call is made to a service. A Fallback can be provided in case a call to the remote service is made when the call rate has already been exceeded

Time Limiter

It makes it possible to limit the amount that can be spent on a call. It is analogous to the timeout when making HTTP calls. A fallback method can be provided in case the time limit is exceeded.

Aspect Order

When you apply multiple patterns on a service call, they execute in a specific order. The default order specified by Resilience4J is:

  1. Bulkhead
  2. Time Limiter.
  3. Rate Limiter.
  4. Circuit Breaker
  5. Retry

Creating Specifications for a Module

Resilience4J Provides two ways to create specifications for any of the above modules: through the application.yml file or Customizer Bean definition. The Bean definition overrides the specifications in the application.yml. Below is an example to define some specifications for a Circuit Breaker Pattern. You can see how we can create specifications for the other modules in the Hand-on-Code section.

Using Application.yml

resilience4j:
  circuitbreaker:
    instances:
      cb-instanceA:
        failure-rate-threshold: 60  #The Threshold Percentage Above Which the Circuit Breaker will move from Closed to Open State.
        wait-duration-in-open-state: 5000  #Time in milliseconds, in which the circuit breaker is to stay in open state before moving to half-open state
        permitted-number-of-calls-in-half-open-state: 10
        minimum-number-of-calls: 10  #The number of calls after which the error rate is calculated. I have assigned it with a small value for test purpose.

Using Bean Definition

   @Bean
   public CircuitBreakerConfigCustomizer circuitBreakerConfigCustomizer() {
       return CircuitBreakerConfigCustomizer.of("cb-instanceB",builder -> builder.minimumNumberOfCalls(10)
       .permittedNumberOfCallsInHalfOpenState(15));
   }

Hands-on-code

Below is a simple project that portrays the use of each pattern. The specifications attributes for each module are pretty self-explanatory. We will define the specifications through the application.yml.

Maven’s pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.resilience</groupId>
    <artifactId>demo</artifactId>
    <version>1.0.0</version>
    <name>Resilience4J Demo</name>
    <description>Demo project for Resilience4J</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-spring-boot2</artifactId>
            <version>1.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Resilience4J requires Spring AOP to make the use of annotations possible.

Application.yml

resilience4j:
  circuitbreaker:
    instances:
      cb-instanceA:
        failure-rate-threshold: 60  #The Threshold Percentage Above Which the Circuit Breaker will move from Closed to Open State.
        wait-duration-in-open-state: 5000  #Time in milliseconds, in which the circuit breaker is to stay in open state before moving to half-open state
        permitted-number-of-calls-in-half-open-state: 10
        minimum-number-of-calls: 10  #The number of calls after which the error rate is calculated. I have assigned it with a small value for test purpose.
  ratelimiter:
    instances:
      rl-instanceA:
        limit-refresh-period: 200ns
        limit-for-period: 40 #The Max number of calls that can be done in the time specified by limit-refresh-period
        timeout-duration: 3s # The max amount of time a call can last
  thread-pool-bulkhead:
    instances:
      tp-instanceA:
        queue-capacity: 2 #The number of calls which can be queued if the thread pool is saturated
        core-thread-pool-size: 4 #The Number of available threads in the Thread Pool.
  timelimiter:
    instances:
      tl-instanceA:
        timeout-duration: 2s # The max amount of time a call can last
        cancel-running-future: false #do not cancel the Running Completable Futures After TimeOut.
  retry:
    instances:
      re-instanceA:
        max-attempts: 3
        wait-duration: 1s # After this time, the call will be considered a failure and will be retried
        retry-exceptions: #The List Of Exceptions That Will Trigger a Retry
          - java.lang.RuntimeException
          - java.io.IOException



Service Class

@Service
public class DemoService {

    @CircuitBreaker(name = "cb-instanceA",fallbackMethod = "cbFallBack")
    public String circuitBreaker() {
        return cbRemoteCall();
    }

    private String cbRemoteCall() {
        double random = Math.random();
        //should fail more than 70% of time
        if (random <= 0.7) {
            throw new RuntimeException("CB Remote Call Fails");
        }
            return "CB Remote Call Executed";
    }

    public String cbFallBack(Exception exception) {
       return String.format("Fallback Execution for Circuit Breaker. Error Message: %s\n",exception.getMessage());
    }

    @RateLimiter(name = "rl-instanceA")
    public String rateLimiter() {
        return "Executing Rate Limited Method";
    }

    @TimeLimiter(name = "tl-instanceA")
    public CompletableFuture<String> timeLimiter() {
        return CompletableFuture.supplyAsync(this::timeLimiterRemoteCall);
    }

    private String timeLimiterRemoteCall() {
        //Will fail 50% of the time
        double random = Math.random();
        if (random < 0.5) {
            return "Executing Time Limited Call...";
        } else {
            try {
                System.out.println("Delaying Execution");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return "Exception Will be Raised";
    }

    @Retry(name = "re-instanceA")
    public String retry() {
        return retryRemoteCall();
    }

    private String retryRemoteCall() {
        //will fail 80% of the time
        double random = Math.random();
        if (random <= 0.8) {
            throw new RuntimeException("Retry Remote Call Fails");
        }

        return  "Executing Retry Remote Call";
    }

    @Bulkhead(name = "tp-instanceA", type = Bulkhead.Type.THREADPOOL)
    public String bulkHead() {
        return "Executing Bulk Head Remote call";
    }
}

Controller Class

@RestController
@RequestMapping("/resilience")
public class DemoController {
    private final DemoService demoService;

    public DemoController(DemoService demoService) {
        this.demoService = demoService;
    }

    @GetMapping("/cb")
    public String circuitBreaker() {
        return demoService.circuitBreaker();
    }

    @GetMapping("/bulkhead")
    public String bulkhead() {
        return demoService.bulkHead();
    }

    @GetMapping("/tl")
    public CompletableFuture<String> timeLimiter() {
        return demoService.timeLimiter();
    }

    @GetMapping("/rl")
    public String rateLimiter() {
        return demoService.rateLimiter();
    }

    @GetMapping("/retry")
    public String retry() {
        return demoService.retry();
    }
}

Conclusion

Beginners usually overlook resilience. I believe that now, you have seen how necessary and useful it is in order to develop a powerful Web Application. I will advise you to explore the specifications for each module so as to see how each can be effectively used.

Hope this tutorial has helped you. See you in the next tutorial.


Leave a Reply

Your email address will not be published. Required fields are marked *

Free Video Lessons

Enter your email and stay on top of things,

Subscribe!