17

I have a method with the following signature:

public Optional<String> doSomething() {
    ...
}

If I get an empty Optional I'd like to retry this method and only after 3 times return the empty Optional.

I've looked and found the Retryable spring annotation, but it seems to only work on Exceptions.

If possible I'd like to use a library for this, and avoid:

  • Creating and throwing an exception.
  • Writing the logic myself.
orirab
  • 2,915
  • 1
  • 24
  • 48
  • You could use recursion and keep track of how many attempts have been tried thus far or call doSomething() from another method that keeps track. – Ethan Roseman Feb 04 '19 at 17:41
  • 1
    I meant more along the lines of an annotation, instead of straight java – orirab Feb 04 '19 at 17:42
  • I'm not aware of a library to do exactly what you're after, but it would be straightforward to write your own annotation to do this, and then re-use it as needed in your code. – Nicholas Hirras Feb 04 '19 at 17:45
  • [`Optional.orElseThrow(...)`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Optional.html#orElseThrow())? There isn't much logic to write... – Turing85 Feb 04 '19 at 17:46
  • Why are you so bounded on returning an `Optional` in the first place? I mean since `@Retryable` works with thrown exceptions, you could very well create your own exception, throw it in case no value is attained and then retry the operation. This way you'll save some time writing your own annotation and such. – akortex Feb 04 '19 at 17:47
  • 1
    throwing exceptions has a high performance cost, for example see here https://stackoverflow.com/questions/299068/how-slow-are-java-exceptions – orirab Feb 04 '19 at 17:57
  • @orirab ["*premature optimization is the root of all evil*" -- Donald Ervin Knuth: *Computer Programming as an Art* (1974), p. 671](https://dl.acm.org/ft_gateway.cfm?id=361612&ftid=289767) – Turing85 Feb 04 '19 at 17:58
  • 1
    this is not premature optimization, you shouldn't use exceptions needlessly anyway – orirab Feb 04 '19 at 17:59

4 Answers4

7

I have been using failsafe build in retry. You can retry based on predicates and exceptions.

Your code would look like this:

    private Optional<String> doSomethingWithRetry() {
        RetryPolicy<Optional> retryPolicy = new RetryPolicy<Optional>()
                .withMaxAttempts(3)
                .handleResultIf(result -> {
                    System.out.println("predicate");
                    return !result.isPresent();
                });

        return Failsafe
                .with(retryPolicy)
                .onSuccess(response -> System.out.println("ok"))
                .onFailure(response -> System.out.println("no ok"))
                .get(() -> doSomething());
    }

    private Optional<String> doSomething() {
         return Optional.of("result");
    }

If the optional is not empty the output is:

predicate
ok

Otherwise looks like:

predicate
predicate
predicate
no ok
  • I've looked a bit at the framework (very cute), but I haven't found a way to implement what I asked. As I understand, your code would mean that if my method returned an empty Optional on the first try, it would not retry at all. If it is empty, I want to retry until it is not empty, or until it has retried 3 times. – orirab Aug 18 '19 at 06:33
  • Hi @orirab. I updated the answer with a tested example. – Cristian Rodriguez Aug 20 '19 at 18:32
  • I'll try it out, looks very nice. If it works well, I'll probably accept your answer. Ty! – orirab Aug 21 '19 at 04:05
3

@Retryable (and the underlying RetryTemplate) are purely based on exceptions.

You could subclass RetryTemplate, overriding doExecute() to check the return value.

You would probably have to replicate much of the code in the method; it's not really designed for overriding just the retryCallback.doWithRetry() call.

You can use a custom RetryTemplate in a RetryOperationsInterceptor (specified in the @Retryable in the interceptor property).

EDIT

The current RetryTemplate code looks like this...

while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {

    try {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Retry: count=" + context.getRetryCount());
        }
        // Reset the last exception, so if we are successful
        // the close interceptors will not think we failed...
        lastException = null;
        return retryCallback.doWithRetry(context);
    }
    catch (Throwable e) {

        lastException = e;

        try {
            registerThrowable(retryPolicy, state, context, e);
        }
        catch (Exception ex) {
            throw new TerminatedRetryException("Could not register throwable",
                    ex);
        }
        finally {
            doOnErrorInterceptors(retryCallback, context, e);
        }

         ... 

    }

You would need to change it to something like...

while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {

    try {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Retry: count=" + context.getRetryCount());
        }
        // Reset the last exception, so if we are successful
        // the close interceptors will not think we failed...
        lastException = null;
        T result = retryCallback.doWithRetry(context);
        if (((Optional<String>) result).get() == null) {

            try {
                registerThrowable(retryPolicy, state, context, someDummyException);
            }
            catch (Exception ex) {
                throw new TerminatedRetryException("Could not register throwable",
                        ex);
            }
            finally {
                doOnErrorInterceptors(retryCallback, context, e);
            }

            ...
        }
        else {
            return result;
        }
    }
    catch (Throwable e) {

       ...

    }

Where someDummyException is to fool the context into incrementing the counter. It can be a static field, just created once.

Gary Russell
  • 166,535
  • 14
  • 146
  • 179
2

I currently wrote a util for this myself (vanilla java), other answers are more than welcome:

import java.util.function.Predicate;
import java.util.function.Supplier;

public class Retryable<T> {
    private Supplier<T> action = () -> null;
    private Predicate<T> successCondition = ($) -> true;
    private int numberOfTries = 3;
    private long delay = 1000L;
    private Supplier<T> fallback = () -> null;

    public static <A> Retryable<A> of(Supplier<A> action) {
        return new Retryable<A>().run(action);
    }

    public Retryable<T> run(Supplier<T> action) {
        this.action = action;
        return this;
    }

    public Retryable<T> successIs(Predicate<T> successCondition) {
        this.successCondition = successCondition;
        return this;
    }

    public Retryable<T> retries(int numberOfTries) {
        this.numberOfTries = numberOfTries;
        return this;
    }

    public Retryable<T> delay(long delay) {
        this.delay = delay;
        return this;
    }

    public Retryable<T> orElse(Supplier<T> fallback) {
        this.fallback = fallback;
        return this;
    }

    public T execute() {
        for (int i = 0; i < numberOfTries; i++) {
            T t = action.get();
            if (successCondition.test(t)) {
                return t;
            }

            try {
                Thread.sleep(delay);
            } catch (InterruptedException e) {
                // do nothing
            }
        }
        return fallback.get();
    }
}

With this code, my method looks like this:

public Optional<String> doSomething() {
    return Retryable
        .of(() -> actualDoSomething())
        .successIs(Optional::isPresent)
        .retries(3)
        .delay(1000L)
        .orElse(Optional::empty)
        .execute();
}
orirab
  • 2,915
  • 1
  • 24
  • 48
0

Just throw Exception if your result is not desired

  • 4
    This answr seems similarily as useful as "You should improve your answer." Though I might be exaggerating a little... – Yunnosch Mar 31 '20 at 04:41