de.mklinger.qetcher.client.impl.retry.Retrier Maven / Gradle / Ivy
package de.mklinger.qetcher.client.impl.retry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.mklinger.qetcher.client.QetcherRemoteException;
import de.mklinger.qetcher.client.common.concurrent.Delay;
/**
* @author Marc Klinger - mklinger[at]mklinger[dot]de
*/
public class Retrier implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(Retrier.class);
private final String id;
private final Supplier> action;
private final CompletableFuture cf;
private final AtomicInteger tries;
private final AtomicReference error;
private final Predicate isRetryCandidate;
private final int maxTries;
private final long maxWaitTimeMillis;
public Retrier(final CompletableFuture cf, final Supplier> action, Predicate isRetryCandidate, int maxTries, final long maxWaitTimeMillis) {
this.id = getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this));
this.cf = cf;
this.action = action;
this.tries = new AtomicInteger(0);
this.error = new AtomicReference<>();
this.isRetryCandidate = isRetryCandidate;
this.maxTries = maxTries;
this.maxWaitTimeMillis = maxWaitTimeMillis;
}
@Override
public void run() {
tries.incrementAndGet();
if (tries.get() > 1 && LOG.isInfoEnabled()) {
LOG.info("{}: Starting try #{}", id, tries.get());
}
action.get()
.thenAccept(this::complete)
.exceptionally(this::handleException);
}
private void complete(T result) {
if (tries.get() > 1 && LOG.isInfoEnabled()) {
LOG.info("{}: Completed successfully after try #{}", id, tries.get());
}
cf.complete(result);
}
private Void handleException(Throwable newError) {
final Throwable e = unwrapCompletionException(newError);
if (!this.error.compareAndSet(null, newRetryFailedException(e))) {
this.error.get().addSuppressed(e);
}
if (tries.get() < maxTries && isRetryCandidate.test(newError)) {
triggerNextTry(newError);
} else {
completeExceptionally();
}
return null;
}
private void triggerNextTry(Throwable newError) {
final long waitTimeMillis = getWaitTimeMillis();
if (LOG.isInfoEnabled()) {
LOG.info("{}: Triggering try #{} in {} millis after exception: {}",
id,
tries.get() + 1,
waitTimeMillis,
unwrapCompletionException(newError).toString());
}
Delay.delayedExecutor(waitTimeMillis, TimeUnit.MILLISECONDS).execute(this);
}
private long getWaitTimeMillis() {
return Math.min(
maxWaitTimeMillis,
50L * tries.get() + 50L + ThreadLocalRandom.current().nextLong(50));
}
private void completeExceptionally() {
// always log final retry errors with stack trace:
if (tries.get() > 1) {
LOG.warn("{}: Completing exceptionally after {} tries", id, tries.get(), this.error.get());
} else {
LOG.warn("{}: Completing exceptionally after first try (no retries)", id, this.error.get());
}
cf.completeExceptionally(this.error.get());
}
private RuntimeException newRetryFailedException(final Throwable newError) {
if (newError instanceof QetcherRemoteException) {
return new RetryFailedQetcherRemoteException((QetcherRemoteException) newError);
} else {
return new RetryFailedGenericException(newError);
}
}
private Throwable unwrapCompletionException(final Throwable error) {
if (error instanceof CompletionException && error.getCause() != null) {
return error.getCause();
} else {
return error;
}
}
}