Retry a method based on result (instead of exception)
@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 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 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();
}