Fault Tolerance

When trying to achieve the goal of resilience in our distributed systems, sometimes we need features like retries, fallbacks, and circuit breakers. Luckily for us, Quarkus provides these features for us through the Fault Tolerance extension.

Add the Fault Tolerance extension

Open a new terminal window, and make sure you’re at the root of your tutorial-app project, then run:

  • Maven

  • Quarkus CLI

./mvnw quarkus:add-extension -D"extensions=quarkus-smallrye-fault-tolerance"
quarkus extension add quarkus-smallrye-fault-tolerance

Add Retry to SwapiService

Let’s add a retry policy in SwapiService.

Change the SwapiService Java interface in src/main/java in the com.redhat.developers package with the following contents:

package com.redhat.developers;

import java.util.List;

import org.eclipse.microprofile.faulttolerance.Retry;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

import com.redhat.developers.Swapi.Results;

@RegisterRestClient
public interface SwapiService {
    @GET
    @Path("/films/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    @Retry(maxRetries = 3, delay = 2000)
    public Swapi getFilmById(@PathParam("id") String id);
}

Now in case of any error, 3 retries are done automatically, waiting for 2 seconds between retries.

Invoke the endpoint with Retry

Run the following command:

curl -w '\n' localhost:8080/movie?year=1980
[
  {
    "title": "The Empire Strikes Back",
    "releaseDate": "1980-05-17",
    "episodeId": 0,
    "producer": "Gary Kurtz, Rick McCallum",
    "director": "Irvin Kershner",
    "opening_crawl": "It is a dark time for the\r\nRebellion. Although the Death\r\nStar has been destroyed,\r\nImperial troops have driven the\r\nRebel forces from their hidden\r\nbase and pursued them across\r\nthe galaxy.\r\n\r\nEvading the dreaded Imperial\r\nStarfleet, a group of freedom\r\nfighters led by Luke Skywalker\r\nhas established a new secret\r\nbase on the remote ice world\r\nof Hoth.\r\n\r\nThe evil lord Darth Vader,\r\nobsessed with finding young\r\nSkywalker, has dispatched\r\nthousands of remote probes into\r\nthe far reaches of space...."
  }
]

No change from calls done previously, but now change the https://swapi.dev url in your application properties to https://swapii.dev (or some other random url that won’t resolve).

Run the following command again:

curl -w '\n' localhost:8080/movie?year=1980

Now after waiting 6 seconds (3 retries x 2 seconds), the next exception is thrown java.net.UnknownHostException: swapii.dev.

Add Fallback to Swapi Service

We can handle unexpected issues with remote services more gracefully by adding a Fallback policy. Let’s add a Fallback handler to SwapiService.

Change the SwapiService Java interface in src/main/java in the com.redhat.developers package with the following contents:

package com.redhat.developers;

import jakarta.ws.rs.*;
import org.eclipse.microprofile.faulttolerance.*;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.core.MediaType;

@RegisterRestClient
public interface SwapiService {

    @GET
    @Path("/films/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    @Timeout(value = 2000L)
    @Fallback(SwapiFallback.class)
    public Swapi getFilmById(@PathParam("id") String id);

    public static class SwapiFallback implements FallbackHandler<Swapi> {

        private static final Swapi EMPTY_SWAPI = new Swapi("",0,"","","");
        @Override
        public Swapi handle(ExecutionContext context) {
            return EMPTY_SWAPI;
        }

    }
}

Now the request will be attempted for 2 seconds. If there is an error, then the fallback method is executed.

Now after waiting for 2 seconds, an empty object is sent instead of an exception.

Invoke the endpoint with Retry and Fallback

Run the following command:

curl -w '\n' localhost:8080/movie?year=1980
[
  {
    "title": "The Empire Strikes Back",
    "releaseDate": "1980-05-17",
    "episodeId": 0,
    "producer": "",
    "director": "",
    "opening_crawl": ""
  }
]

Notice how we’re still returning the results from our database, but the remote service values are now empty as they are set by our fallback method.

Add Circuit Breaker to Swapi Service

Let’s add the circuit breaker policy in SwapiService.

Change the SwapiService Java interface in src/main/java in the com.redhat.developers package with the following contents:

package com.redhat.developers;

import jakarta.ws.rs.*;
import org.eclipse.microprofile.faulttolerance.*;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.core.MediaType;

@RegisterRestClient
public interface SwapiService {
    @GET
    @Path("/films/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    @Timeout(value = 2000L)
    @Fallback(SwapiFallback.class)
    @CircuitBreaker(
            requestVolumeThreshold = 4,
            failureRatio = .5,
            delay = 5000L,
            successThreshold = 2
    )
    public Swapi getFilmById(@PathParam("id") String id);

    public static class SwapiFallback implements FallbackHandler<Swapi> {

        private static final Swapi EMPTY_SWAPI = new Swapi("",0,"","","");
        @Override
        public Swapi handle(ExecutionContext context) {
            return EMPTY_SWAPI;
        }

    }
}

Now, if 3 (4 x 0.75) failures occur among the rolling window of 4 consecutive invocations, then the circuit is opened for 5000 ms and then will be back to half open. If the invocation succeeds, then the circuit is back to closed again.

Run the following command at least 5 times (without network connectivity):

curl -w '\n' localhost:8080/movie?year=1980

The output changes from java.net.UnknownHostException: swapii.dev (or any other network exception) in the first calls to org.eclipse.microprofile.faulttolerance.exceptions.CircuitBreakerOpenException: getMovieByTitle when the circuit is opened.

The big difference between the first exception and the second one is that the first one occurs because the circuit is closed while the system is trying to reach the host, while in the second one, the circuit is closed and the exception is thrown automatically without trying to reach the host.

You can use @Retry and @Fallback annotations together with @CircuitBreaker annotation.
If you turned your network off for this chapter, remember to turn it back on again after you finish the exercises for this chapter.