14

In my code I have a section which tries to connect to some external interface, if it fails, then it will retry it a fixed number of times. The code works, but is somewhat ugly. I am wondering whether this can be done in a more elegant way using some fancy Java8 features?

int count = 0;
final int maxRetries = 3;
while ( count < maxRetries )
{
   try
   {
     // Some Code
     // break out of loop on success
   }
   catch ( final ExecutionException e )
   {
      LOG.debug( "retrying..." );
      if ( ++count >= maxRetries )
      {
         LOG.debug( "do something else...");
         //do something else
      }
   }
}
Tunaki
  • 132,869
  • 46
  • 340
  • 423
Moonlit
  • 5,171
  • 14
  • 57
  • 95
  • 2
    What is ugly in this code? To retry, you need to loop (or recurse). (related: http://stackoverflow.com/questions/34740091/apply-retries-in-a-rxjava and http://stackoverflow.com/questions/30989558/java-8-retry-a-method-until-a-condition-is-fulfilled-in-intervals) – Tunaki Jun 29 '16 at 17:12
  • Yeah I know, maybe I am too fussy here, but I am just wondering whether this can be done in a more "functional" or "declarative" way, because this is often more readable... – Moonlit Jun 29 '16 at 17:22
  • 2
    What's ugly is that it conflates the code for a particular task with the retry / failure management logic. That means you would duplicate the retry logic all over the place, probably by cut and paste, making it virtually impossible to adjust your retry behavior consistently later. – Brian Goetz Jun 29 '16 at 18:31
  • 2
    @BrianGoetz Yes, that is very true. Since the OP didn't post the code in `// Some code`, I rather interpreted the question as trying to improve the retry logic itself (maybe with functional idioms). – Tunaki Jun 29 '16 at 18:52

5 Answers5

29

What you can do is separate out the retry logic. You'll need some ancillary scaffolding:

interface ThrowingTask {
    void run() throws ExecutionException;
}

Now, you write:

boolean runWithRetries(int maxRetries, ThrowingTask t) { 
    int count = 0;
    while (count < maxRetries) {
        try {
            t.run();
            return true;
        }
        catch (ExecutionException e) {
            if (++count >= maxRetries)
                return false;
        }
    }
}

Now, you can run things with retries without having to conflate your task logic with your retry logic:

runWithRetries(MAX_RETRIES, () -> { /* do stuff */ });

You can tweak this as you like to accept lambdas which are called on retry, return the retry count, etc etc. But the game is to write methods like runWithRetries which capture the control flow but abstract over what behavior needs to be done -- so you only have to write your retry loop once, and then fill in the actual behavior you want wherever needed.

Brian Goetz
  • 90,105
  • 23
  • 150
  • 161
  • Here the assumption is that the external method will be of type void fun(); What if I need same retry logic for all my methods? for example same retry logic for void fun(int a); and void fun(String s, int a) ? – Priyank Doshi Apr 01 '19 at 16:04
  • @PriyankDoshi you can use a callable interface instead which returns a value – webh Jul 22 '21 at 15:38
16

Using Failsafe (which I authored):

RetryPolicy retryPolicy = new RetryPolicy()
  .retryOn(ExecutionException.class)
  .withMaxRetries(3);

Failsafe.with(retryPolicy)
  .onRetry(r -> LOG.debug("retrying..."))
  .withFallback(e -> LOG.debug("do something else..."))
  .run(() -> someCode());

It's about as simple and expressive as you can get for your use case.

Jonathan
  • 5,027
  • 39
  • 48
  • Nice, clean API. Can definitely recommend! – Dormouse May 06 '19 at 15:04
  • Added the method someCode(). Getting the compilation error at the very end: Syntax error, insert "}" to complete MethodBody – Alex Jan 19 '20 at 21:17
  • just incorporated this into my project. Works awesome! The library API looks to have changed so I just picked up examples from the website – alex May 14 '21 at 02:43
7

Well, more functional approach in my opinion will be to use Trymonad which unfortunately is not there for us in jdk 8 :(

Nevertheless you still can use better-monads library which provides it. Having that you can come up with some implementation like this:

public static <Out> Try<Out> tryTimes(int times, TrySupplier<Out> attempt) {
        Supplier<Try<Out>> tryAttempt = () -> Try.ofFailable(attempt::get);

        return IntStream.range(1, times)
                .mapToObj(i -> tryAttempt)
                .reduce(tryAttempt, (acc, current) -> () -> acc.get().recoverWith(error -> current.get()))
                .get();
    }

Long story short this function just chains calls of tryAttempt and in case of failed attempt tries to recoverWith the next call of tryAttempt. Client code is going to look like this:

tryTimes(10, () -> {
            // all the logic to do your possibly failing stuff
        }
);

As a result client code is going to get Try<T> which can be unpacked by direct call of .get() (in case of success returns the value, in case of failure throws underlying exception) or with other methods described in library documentation.

Hope it helps.

UPDATE:

This can be done also in functional way using the filter, findFirst and limit and without any external libraries:

interface ThrowingSupplier<Out> { Out supply() throws Exception; }

public static <Out> Optional<Out> tryTimes(int times, ThrowingSupplier<Out> attempt) {
    Supplier<Optional<Out>> catchingSupplier = () -> {
        try {
            return Optional.ofNullable(attempt.supply());
        } catch (Exception e) {
            return Optional.empty();
        }
    };
    return Stream.iterate(catchingSupplier, i -> i)
            .limit(times)
            .map(Supplier::get)
            .filter(Optional::isPresent)
            .findFirst()
            .flatMap(Function.identity());
}

The client code remains the same. Also, please note that it is not going to evaluate expression times times, but will stop on the first successful attempt.

tkachuko
  • 1,956
  • 1
  • 13
  • 20
  • For the second solution, is it possible to use the first solution's IntStream.range(0, times).mapToObj(i -> catchingSupplier) to generate the bound catchingSupplier stream? – srborlongan Jun 30 '16 at 01:23
  • 1
    @srborlongan yes it is. I just wanted to keep second solution as minimalistic as possible, meaning that I do not really like mentioning of 0 and IntStream (not very helpful for understanding IMO) at all and prefer limit method. – tkachuko Jun 30 '16 at 01:29
0

Similar to some of the suggested approaches, you could create a class that separates the retry functionality from the rest of you code. The class below that does just that and also allows you to take an input and return some result:

public class Retrier<T, R> {
    private static final int DEFAULT_RETRY_COUNT = 3;
    private int retryCount;
    private Function<T, R> retriable;
    private T input;

    public Retrier(T input, Function<T, R> retriable) {
        this(input, retriable, DEFAULT_RETRY_COUNT);
    }

    public Retrier(T input, Function<T, R> retriable, int retryCount) {
        this.retryCount = retryCount;
        this.retriable = retriable;
        this.input = input;
    }

    public R execute() {
        int count = 0;
        while(true) {
            try {
                return retriable.apply(input);
            }
            catch (Exception e) {
                if (++count >= retryCount) {
                    throw e;
                }
            }
        }
    }
}

And then you can use it like this e.g.

String result = new Retrier<String, String>("input", input -> {/* process the input and return the result*/}).execute();

And wherever your code will throw an exception it will be retried.

Community
  • 1
  • 1
v0rin
  • 510
  • 4
  • 11
  • 1
    I tried this but my code within the brackets thows a SQLException so my IDE wanted me to add a try catch which defeats the purpose. I am guessing I am misunderstanding this... `resultSet = new Retrier(preparedStatement, input -> {return input.executeQuery();}, 2).execute();` – Kachopsticks Jun 26 '20 at 17:24
  • I think you got it right. But whenever your Retriable function throws some checked exception you need to wrap it in a try-catch block and rethrow as an unchecked exception e.g. RuntimeException. The only workaround would be extending the Function interface with your own one that throws SQLException. That is doable but that would make the Retrier class less universal – v0rin Jun 28 '20 at 14:46
0

You can try this static method

/**
 * Author: Mak Sophea
 * Date: 07/17/2020
 */
public class RetryUtils {
    private static final Logger log = LoggerFactory.getLogger(RetryUtils.class);

    public static interface CallToRetry {
        void process() throws Exception;
    }

    public static boolean withRetry(int maxTimes, long intervalWait, CallToRetry call) throws Exception {
        if (maxTimes <= 0) {
            throw new IllegalArgumentException("Must run at least one time");
        }
        if (intervalWait <= 0) {
            throw new IllegalArgumentException("Initial wait must be at least 1");
        }
        Exception thrown = null;
        for (int i = 0; i < maxTimes; i++) {
            try {
                call.process();
                return true;
            } catch (Exception e) {
                thrown = e;
                log.info("Encountered failure on {} due to {}, attempt retry {} of {}", call.getClass().getName() , e.getMessage(), (i + 1), maxTimes, e);
            }
            try {
                Thread.sleep(intervalWait);
            } catch (InterruptedException wakeAndAbort) {
                break;
            }
        }
        throw thrown;
    }
}

You can call it by RetryUtils.withRetry(MAX_RETRY, 3000, () -> { // put the implementation here });

sopheamak
  • 393
  • 3
  • 12