Exponential Backoff Algorithm with Full Jitter Java Implementation

Viewed 5919

I am trying to create a utility class in pure Java that will contain the logic needed for the Exponential Backoff Algorithm implementation with full jitter as there will be multiple clients sending requests. I have another class with a method that executes a GET or a POST request and returns a response with a status code. I want to retry (aka use the exponential backoff strategy) only if the status code is in the 5xx. Current code is not compiling.

The calling method looks like this:

HttpResponse response = executeGetRequest( params );
int statusCode = response.getStatusCode();
//some status code validation

My ExponentialBackoffStrategy class is:

public class ExponentialBackoffStrategy {

private final long maxBackoff;

private long backoffValue;

private long attempts;
private static final long DEFAULT_MAX_RETRIES = 900_000;

private Random random = new Random();

public ExponentialBackoffStrategy( long maxBackoff ) {
    this.maxBackoff = maxBackoff;
}

public long getWaitTimeExp() {
    if( backoffValue >= maxBackoff ) {
        return maxBackoff;
    }
    double pow = Math.pow( 2, attempts++ );
    int rand = random.nextInt( 1000 );
    backoffValue = ( long ) Math.min( pow + rand, maxBackoff );
    return backoffValue;
}

public static ExponentialBackoffStrategy getDefault() {
    return new ExponentialBackoffStrategy( DEFAULT_MAX_RETRIES );
    }
}

I want to get some feedback on the implemented class in regards to if I can do anything better and also how to integrate it with the caller method. My idea as of right now is:

ExponentialBackoffStrategy backoff = ExponentialBackoffStrategy.getDefault();
boolean retry = false;
HttpResponse response = null;
int statusCode = 0;
do {
  response = executeGetRequest( params );
  statusCode = response.getStatusLine().getStatusCode();
  if( statusCode >= 500 && statusCode < 600 ) {
    retry = true;
    try {
        Thread.sleep( backoff.getWaitTimeExp() );
    } catch ( InterruptedException e ) {
        //handle exception
    }
  }
} while ( retry );

Any help would be greatly appreciated!

EDIT: The response is actually located in try with resources.

try ( HttpResponse response = backoff.attempt(
() -> executeGetRequest( params ),
r -> {
  final int statusCode = response.getStatusLine().getStatusCode();
  return statusCode < 500 || statusCode >= 600;
}
);)

I am running into two issues:

  1. On the line final int statusCode = response.getStatusLine().getStatusCode(); "response" is underlined red "variable 'response' may not have been initialized". Tried to take it outside the try block and try with resources doesn't like it.
  2. executeGetRequest now needs a catch block inside of the lambda: try ( HttpResponse response = executePostRequest( params ) ) {
2 Answers

You could bring more of your boilerplate inside your class, for example:

public class ExponentialBackoffStrategy {
...
    @Nullable
    public <T> T attempt(Supplier<T> action, Predicate<T> success) {
        int attempts = 0;

        T result = action.get();
        while (!success.test(result)) {
            try {
                Thread.sleep(getWaitTimeExp(attempts++));
            } catch ( InterruptedException e ) {
                //handle exception
            }
            result = action.get();
        }
        return result;
    }
}

which you would then use like this:

        ExponentialBackoffStrategy backoff = ExponentialBackoffStrategy.getDefault();
        final HttpResponse response = backoff.attempt(
                () -> executeGetRequest( params ),
                r -> {
                    final int statusCode = r.getStatusLine().getStatusCode();
                    return statusCode < 500 || statusCode >= 600;
                }
        );

This reduces the amount of repeated code in your program, and the retry logic can be tested once.

I have moved the mutable state (attempts, and backoffValue can be removed) out of the class and into a local variable in the attempt() function. This means that a single ExponentialBackoffStrategy instance can be safely reused, and also used by multiple threads. So getWaitTimeExp becomes a function with no side-effects:

    private long getWaitTimeExp(int attempts) {
        final double pow = Math.pow( 2, attempts);
        final int rand = random.nextInt( 1000 );
        return ( long ) Math.min( pow + rand, maxBackoff );
    }

This is untested code!

You should probably stop retrying after some number of retries too.

To test this you. would want to put both the sleeping and the random number generation into separate components which are injected into ExponentialBackoffStrategy. Your static factory method can inject the production implementations, and your test would use the ExponentialBackoffStrategy constructor and pass mocks.

So you'd have the interfaces:

interface RandomNumber {
   int next();
}

interface Sleeper {
   void sleep(long milliseconds);
}

and a constructor:

protected ExponentialBackoffStrategy(long maxBackoff, RandomNumber randomNumber, Sleeper sleeper) {
...
}

The above one is an Exponential Backoff but without a random sleep time. I have tried it to give random time.

if this logic looks good and I wanted to have the max interval time frame.

if tell mx_interal as 2000L - Then for the first attempt it should be 2000L, the second attempt if the calculation should fall between 1 > < 2000L. So that it is always between this range.

private long getWaitTimeExp(int retries) {         
    return getWaitTimeExp(retries, withDelayBetweenTries(maxWaitInterval, ChronoUnit.MILLIS)).toMillis();
}
    public Duration getWaitTimeExp(int numberOfTriesFailed, Duration delayBetweenAttempts) {
    int i = ThreadLocalRandom.current().nextInt(1, this.maxMultiplier);
    return getDurationToWait(numberOfTriesFailed, Duration.ofMillis(i * delayBetweenAttempts.toMillis()));
}

public Duration getDurationToWait(int numberOfTriesFailed, Duration delayBetweenAttempts) {
    double exponentialMultiplier = Math.pow(2.0, numberOfTriesFailed);
    double result = exponentialMultiplier * delayBetweenAttempts.toMillis();
    long millisToWait = (long) Math.min(result, Long.MAX_VALUE);
    return Duration.ofMillis(millisToWait);
}

public static Duration withDelayBetweenTries(long amount, ChronoUnit time) {
    return Duration.of(amount, time);
}
Related