Retry a method based on result (instead of exception)
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:
predicateok
Otherwise looks like:
predicatepredicatepredicateno 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.
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();}