Home

Retrying specific code on an Exception/Error in Java

Introduction

This is already the second Java post within a few days. This time, I want to demonstrate, how to retry some specific code when an Exception or an Error is thrown. This might be helpful when dealing with distributed systems and a first call didn’t bring success. My first attempts in researching a solution for this were not very satisfying. Most times you only could retry the whole method or just use a simple loop. But I wanted something more granular and more extensible without copying the same loops over and over again. The closest solution I found was the “enterprisy” Stackoverflow answer by ach - which lost against the simple loop as the correct answer ;-). In this post, I want to present a slightly enhanced version of the retry functionality with Java 8 compatibility.

Example

For Throwables (Exceptions and Errors)

      // some code
      // ...
      Retry.onThrowable(5, new RetryOperation() {
         @Override public void doIt(int attempt) throws Exception {
            // Code which will be retried.
            // A test with Assert.assertNull(new Object()) will retry
            // the code inside here until no Exception or Error is thrown
            // or the maximum attempts of 5 are exceeded.
         }
      });
      // some more code
      // ...

For Exceptions only

      // some code
      // ...
      Retry.onException(5, new RetryOperation() {
         @Override public void doIt(int attempt) throws Exception {
            // Code which will be retried.
            // A test with Assert.assertNull(new Object()) will imidiately fail
            // as the assertion throws an Error and not an Exception,
            // but we are calling 'onException' instead of 'onThrowable'.
         }
      });
      // some more code
      // ...

The implementation could be further enhanced, e.g. it should be possible to only retry on some specific Throwables (blacklist) or not retry on specific Throwables (whitelist).

Code

The code is very simple and only consists of 3 classes (with an additional third test class).

Retry.java

The actual retry logic for Exceptions and Throwables (Exceptions and Errors).

public class Retry {

   /**
    * based on https://stackoverflow.com/a/13240586
    *
    * @param maxAttempts (max. value is 7)
    * @param retryOperation
    * @throws Exception
    */
   public static void onException(int maxAttempts, RetryOperation retryOperation) throws Exception {
      if (maxAttempts > 7) {
         // TODO: make this configurable
         maxAttempts = 7;
      }

      for (int attempt = 0; ; attempt++) {
         try {
            retryOperation.doIt(attempt);
            break; // call was successful
         } catch (Exception e) {
            if (attempt < maxAttempts) {
               // exponential backoff before trying again
               exponentialSleep(attempt+1);
               continue;
            }
            // reached max. attempts
            throw e;
         }
      }
   }


   /**
    * Suitable for tests as junit's assert functions throws errors and not exceptions. Throwable will catch both, errors and exceptions
    * but shouldn't be used in non-testing code.
    *
    * @param maxAttempts
    * @param retryOperation
    * @throws Exception
    */
   public static void onThrowable(int maxAttempts, RetryOperation retryOperation) throws Throwable {
      if (maxAttempts > 7) {
         // TODO: make this configurable
         maxAttempts = 7;
      }

      for (int attempt = 0; ; attempt++) {
         try {
            retryOperation.doIt(attempt);
            break; // call was successful
         } catch (Throwable t) {
            if (attempt < maxAttempts) {
               // exponential backoff before trying again
               exponentialSleep(attempt+1);
               continue;
            }
            // reached max. attempts
            throw t;
         }
      }
   }

   // using 6: limit max wait time (2^6 *100ms =  6,4 seconds in the last attempt)
   // using 7: limit max wait time (2^7 *100ms = 12,8 seconds in the last attempt)
   // based on https://docs.aws.amazon.com/en_us/general/latest/gr/api-retries.html
   private static void exponentialSleep(int count) throws InterruptedException {
      Thread.sleep(((long) Math.pow(2, count) * 100L));
   }
}

RetryOperation.java

The abstract class to implement with your own code which should be retried on Exceptions (see Example).

package io.gitlab.sj14.retry;

/**
 * based on https://stackoverflow.com/a/13240586
 */

public abstract class RetryOperation {

   /**
    * Retryable code to execute.
    * @param attempt counts the current attempt, starting at 0.
    * @throws Exception
    */
   abstract public void doIt(int attempt) throws Exception;
}

RetryTester.java

Just a test class.

import org.junit.Test;

public class RetryTester {

  @Test
  public void testRetryExceptionSuccess() throws Exception {
    Retry.onException(3, new RetryOperation() {

      @Override
      public void doIt(int attempt) throws Exception {
          // First 2 tries fail,
          // third and last try succeeds.
          if (attempt < 2) {
            throw new java.lang.Exception();
        }
      }
    });
  }

  @Test(expected = java.lang.Exception.class)
  public void testRetryExceptionFail() throws Exception {
    Retry.onException(3, new RetryOperation() {

      @Override
      public void doIt(int attempt) throws Exception {
          throw new java.lang.Exception();
      }
    });
  }

  @Test(expected = java.lang.AssertionError.class)
  public void testRetryExceptionFailError() throws Exception {
    Retry.onException(3, new RetryOperation() {

      @Override
      public void doIt(int attempt) throws Exception {
        if (attempt < 2) {
          // Doesn't retry as it's an Error and we are using Retry.onException.
          // The first throw will already exit this test.
          throw new java.lang.AssertionError();
        }
      }
    });
  }

  @Test
  public void testRetryThrowableSuccess() throws Throwable {
    Retry.onThrowable(3, new RetryOperation() {

      @Override
      public void doIt(int attempt) throws Exception {
        // First 2 tries fail,
        // third and last try succeeds.
        if (attempt < 2) {
          throw new java.lang.AssertionError();
        }
      }
    });
  }

  @Test(expected = java.lang.AssertionError.class)
  public void testRetryThrowableFail() throws Throwable {
    Retry.onThrowable(3, new RetryOperation() {

      @Override
      public void doIt(int attempt) throws Exception {
          // throw Error instead of Exception
          throw new java.lang.AssertionError();
      }
    });
  }
}

Home · RSS · E-Mail · GitHub · GitLab · Twitter