package com.nurkiewicz.asyncretry;
import com.nurkiewicz.asyncretry.backoff.Backoff;
import com.nurkiewicz.asyncretry.policy.AbortRetryException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* @author Tomasz Nurkiewicz
* @since 7/21/13, 6:10 PM
*/
public abstract class RetryJob<V> implements Runnable {
private static final Logger log = LoggerFactory.getLogger(RetryJob.class);
protected final CompletableFuture<V> future;
protected final AsyncRetryContext context;
protected final AsyncRetryExecutor parent;
public RetryJob(AsyncRetryContext context, AsyncRetryExecutor parent, CompletableFuture<V> future) {
this.context = context;
this.parent = parent;
this.future = future;
}
protected void logSuccess(RetryContext context, V result, long duration) {
log.trace("Successful after {} retries, took {}ms and returned: {}", context.getRetryCount(), duration, result);
}
protected void handleManualAbort(AbortRetryException abortEx) {
logAbort(context);
if (context.getLastThrowable() != null) {
future.completeExceptionally(context.getLastThrowable());
} else {
future.completeExceptionally(abortEx);
}
}
protected void logAbort(RetryContext context) {
log.trace("Aborted by user after {} retries", context.getRetryCount() + 1);
}
protected void handleThrowable(Throwable t, long duration) {
if (t instanceof AbortRetryException) {
handleManualAbort((AbortRetryException) t);
} else {
handleUserThrowable(t, duration);
}
}
protected void handleUserThrowable(Throwable t, long duration) {
final AsyncRetryContext nextRetryContext = context.nextRetry(t);
try {
retryOrAbort(t, duration, nextRetryContext);
} catch (Throwable predicateError) {
log.error("Threw while trying to decide on retry {} after {}",
nextRetryContext.getRetryCount(),
duration,
predicateError);
future.completeExceptionally(t);
}
}
private void retryOrAbort(Throwable t, long duration, AsyncRetryContext nextRetryContext) {
if (parent.getRetryPolicy().shouldContinue(nextRetryContext)) {
final long delay = calculateNextDelay(duration, nextRetryContext, parent.getBackoff());
retryWithDelay(nextRetryContext, delay, duration);
} else {
logFailure(nextRetryContext, duration);
future.completeExceptionally(t);
}
}
protected void logFailure(AsyncRetryContext nextRetryContext, long duration) {
log.trace("Giving up after {} retries, last run took: {}ms, last exception: ",
context.getRetryCount(),
duration,
nextRetryContext.getLastThrowable());
}
private long calculateNextDelay(long taskDurationMillis, AsyncRetryContext nextRetryContext, Backoff backoff) {
final long delay = backoff.delayMillis(nextRetryContext);
return delay - (parent.isFixedDelay()? taskDurationMillis : 0);
}
private void retryWithDelay(AsyncRetryContext nextRetryContext, long delay, long duration) {
final RetryJob<V> nextTask = nextTask(nextRetryContext);
parent.getScheduler().schedule(nextTask, delay, MILLISECONDS);
logRetry(nextRetryContext, delay, duration);
}
protected void logRetry(AsyncRetryContext nextRetryContext, long delay, long duration) {
final Date nextRunDate = new Date(System.currentTimeMillis() + delay);
log.trace("Retry {} failed after {}ms, scheduled next retry in {}ms ({})",
context.getRetryCount(),
duration,
delay,
nextRunDate,
nextRetryContext.getLastThrowable());
}
@Override
public void run() {
run(System.currentTimeMillis());
}
protected abstract void run(long startTime);
protected abstract RetryJob<V> nextTask(AsyncRetryContext nextRetryContext);
protected void complete(V result, long duration) {
logSuccess(context, result, duration);
future.complete(result);
}
public CompletableFuture<V> getFuture() {
return future;
}
}