Design pattern for "retrying" logic that failed?

Viewed 64991

I'm writing some reconnect logic to periodically attempt to establish a connection to a remote endpoint which went down. Essentially, the code looks like this:

public void establishConnection() {
    try {
        this.connection = newConnection();
    } catch (IOException e) {
        // connection failed, try again.
        try { Thread.sleep(1000); } catch (InterruptedException e) {};

        establishConnection();
    }
}

I've solved this general problem with code similar to the above on many occasions, but I feel largely unsatisfied with the result. Is there a design pattern designed for dealing with this issue?

11 Answers

I really like this Java 8 code from this blog and you don't need any extra library on your classpath.

You only need to pass a function to the retry class.

@Slf4j
public class RetryCommand<T> {

    private int maxRetries;

    RetryCommand(int maxRetries)
    {
        this.maxRetries = maxRetries;
    }

    // Takes a function and executes it, if fails, passes the function to the retry command
    public T run(Supplier<T> function) {
        try {
            return function.get();
        } catch (Exception e) {
            log.error("FAILED - Command failed, will be retried " + maxRetries + " times.");
            return retry(function);
        }
    }

    private T retry(Supplier<T> function) throws RuntimeException {

        int retryCounter = 0;
        while (retryCounter < maxRetries) {
            try {
                return function.get();
            } catch (Exception ex) {
                retryCounter++;
                log.error("FAILED - Command failed on retry " + retryCounter + " of " + maxRetries, ex);
                if (retryCounter >= maxRetries) {
                    log.error("Max retries exceeded.");
                    break;
                }
            }
        }
        throw new RuntimeException("Command failed on all of " + maxRetries + " retries");
    }
}

And to use it:

new RetryCommand<>(5).run(() -> client.getThatThing(id));

You can also create a wrapper function that just does a loop over the intended operation and when is done just break out of the loop.

public static void main(String[] args) {
    retryMySpecialOperation(7);
}

private static void retryMySpecialOperation(int retries) {
    for (int i = 1; i <= retries; i++) {
        try {
            specialOperation();
            break;
        }
        catch (Exception e) {
            System.out.println(String.format("Failed operation. Retry %d", i));
        }
    }
}

private static void specialOperation() throws Exception {
    if ((int) (Math.random()*100) % 2 == 0) {
        throw new Exception("Operation failed");
    }
    System.out.println("Operation successful");
}

Here's a another approach to perform the retry. No libraries, no annotations, no extra implementations. Import java.util.concurrent.TimeUnit;

public static void myTestFunc() {
        boolean retry = true;
        int maxRetries = 5;   //max no. of retries to be made
        int retries = 1;
        int delayBetweenRetries = 5;  // duration  between each retry (in seconds)
        int wait = 1;
    do {
        try {
            this.connection = newConnection();
            break;
        }
        catch (Exception e) {
            wait = retries * delayBetweenRetries;
            pause(wait);
            retries += 1;
            if (retries > maxRetries) {
                retry = false;
                log.error("Task failed on all of " + maxRetries + " retries");
            }
        }
    } while (retry);

}

public static void pause(int seconds) {
    long secondsVal = TimeUnit.MILLISECONDS.toMillis(seconds);

    try {
        Thread.sleep(secondsVal);
    }
    catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
    }
}

}

I'm using retry4j library. Test code example:

public static void main(String[] args) {
    Callable<Object> callable = () -> {
        doSomething();
        return null;
    };

    RetryConfig config = new RetryConfigBuilder()
            .retryOnAnyException()
            .withMaxNumberOfTries(3)
            .withDelayBetweenTries(5, ChronoUnit.SECONDS)
            .withExponentialBackoff()
            .build();

    new CallExecutorBuilder<>().config(config).build().execute(callable);
}

public static void doSomething() {
    System.out.println("Trying to connect");
    // some logic
    throw new RuntimeException("Disconnected"); // init error
    // some logic
}
Related