3

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 ) ) {
unboundedcauchy
  • 113
  • 2
  • 8
  • Have you considered debugging your code? It doesn't work. You need to break out of your `do/while` look on success, or at least clear `retry`. You don't need `Math.pow()` when `<< 1` would do the same thing. – user207421 Mar 02 '21 at 23:09
  • Thank you for your comment. Unfortunately, this introduced chunk is deep within a very large code-base that I am unable to run/debug locally. I can only unit test and I wanted to get some feedback beforehand. – unboundedcauchy Mar 02 '21 at 23:15
  • I see from your edit that your code doesn't even *compile.* This should have been stated up front. – user207421 Mar 02 '21 at 23:16
  • Fair point, edited. – unboundedcauchy Mar 02 '21 at 23:20
  • 1. was a typo in my answer -- the predicate lambda should be using r. 2. Yes, if executePostRequest can throw an exception then you'll need a try catch, and you'll presumably return null and your predicate will need to handle null. Or maybe you'll decide that you should return an object that can contain either an HttpRespone or an Exception. – tgdavies Mar 02 '21 at 23:23
  • Thank you @tgdavies! – unboundedcauchy Mar 02 '21 at 23:24
  • @unboundedcauchy Glad to help, you can consider accepting my answer if you feel it is helpful. – tgdavies Mar 03 '21 at 05:51

2 Answers2

5

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) {
...
}
tgdavies
  • 10,307
  • 4
  • 35
  • 40
0

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);
}
Java Carzy
  • 65
  • 1
  • 7