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:
1. Simple for-loop with try-catch. A simple solution to implement retry logic in Java is to write your code inside a for loop that executes the specified number of times (the maximum retry value).
A retryable exception is a transient exception that if retried may succeed.
First, you need to enable Spring Retry. You can achieve this by adding the @EnableRetry annotation to your @SpringBootApplication or @Configuration class. You can now use @Retryable to annotate any method to be a candidate or retry and @Recover to specify fallback methods.
Spring Retry provides the ability to automatically re-invoke a failed operation. This is helpful when errors may be transient in nature. For example, a momentary network glitch, network outage, server down, or deadlock. You can configure the. spring-retry.
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
                        @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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With