package com.linkedin.parseq.retry;
import com.linkedin.parseq.TaskType;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.linkedin.parseq.Context;
import com.linkedin.parseq.Priority;
import com.linkedin.parseq.Task;
import com.linkedin.parseq.function.Function1;
import com.linkedin.parseq.internal.ArgumentUtil;
import com.linkedin.parseq.promise.Promise;
import com.linkedin.parseq.promise.Promises;
import com.linkedin.parseq.promise.SettablePromise;
/**
* A parseq task wrapper that supports arbitrary retry policies.
*
* @author Oleg Anashkin (oleg.anashkin@gmail.com)
*/
public final class RetriableTask<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(RetriableTask.class);
/** A name of the task that needs to be retried. */
private final String _name;
/** A task generator function. It will receive a zero-based attempt number as a parameter. */
private final Function1<Integer, Task<T>> _taskFunction;
/** Retry policy which will control this task's behavior. */
private final RetryPolicy _policy;
/** Start time for the very first attempt. */
private long _startedAt;
/**
* A parseq task wrapper that supports arbitrary retry policies.
*
* @param name A name of the task that needs to be retried.
* @param policy Retry policy that will control this task's behavior.
* @param taskFunction A task generator function. It will receive a zero-based attempt number as a parameter.
*/
private RetriableTask(String name, RetryPolicy policy, Function1<Integer, Task<T>> taskFunction)
{
ArgumentUtil.requireNotNull(name, "name");
ArgumentUtil.requireNotNull(policy, "policy");
ArgumentUtil.requireNotNull(taskFunction, "taskFunction");
_name = name;
_policy = policy;
_taskFunction = taskFunction;
}
/**
* A helper for creating task wrapper with associated retry policy.
*
* @param name A name of the task that needs to be retried.
* @param policy Retry policy that will control this task's behavior.
* @param taskFunction A task generator function. It will receive a zero-based attempt number as a parameter.
* @param <U> Type of a task result.
*/
public static <U> Task<U> withRetryPolicy(String name, RetryPolicy policy, Function1<Integer, Task<U>> taskFunction) {
RetriableTask<U> retriableTask = new RetriableTask<>(name, policy, taskFunction);
Task<U> retryTaskWrapper = Task.async(name + " retriableTask", retriableTask::run);
retryTaskWrapper.getShallowTraceBuilder().setTaskType(TaskType.WITH_RETRY.getName());
return retryTaskWrapper;
}
/** Create a wrapped task with associated recovery task that will retry if necessary. */
private Task<T> wrap(int attempt) {
Task<T> retryTask = Task.async(_policy.getName() + ", attempt " + attempt, context -> {
final SettablePromise<T> result = Promises.settable();
Task<T> task = _taskFunction.apply(attempt);
final Task<T> recovery = Task.async(_name + " recovery", recoveryContext -> {
final SettablePromise<T> recoveryResult = Promises.settable();
if (task.isFailed()) {
// Failed task will cause retry to be scheduled.
ErrorClassification errorClassification = _policy.getErrorClassifier().apply(task.getError());
retry(attempt + 1, task.getError(), errorClassification, recoveryContext, recoveryResult);
} else {
recoveryResult.done(task.get());
}
return recoveryResult;
});
// Recovery task should run immediately after the original task to process its error.
recovery.setPriority(Priority.MAX_PRIORITY);
recovery.getShallowTraceBuilder().setSystemHidden(true);
Promises.propagateResult(recovery, result);
context.after(task).run(recovery);
context.run(task);
return result;
});
retryTask.getShallowTraceBuilder().setTaskType(TaskType.RETRY.getName());
return retryTask;
}
/** Invoke event monitors and schedule a retry if policy allows. */
private void retry(int attempt, Throwable error, ErrorClassification errorClassification, Context recoveryContext, SettablePromise<T> recoveryResult) {
long backoffTime = _policy.getBackoffPolicy().nextBackoff(attempt, error);
if (errorClassification == ErrorClassification.UNRECOVERABLE) {
// For fatal errors there are no retries.
LOGGER.debug(String.format("Attempt %s of %s interrupted: %s", attempt, _name, error.getMessage()));
recoveryResult.fail(error);
} else if (_policy.getTerminationPolicy().shouldTerminate(attempt, System.currentTimeMillis() - _startedAt + backoffTime)) {
// Retry policy commands that no more retries should be done.
LOGGER.debug(String.format("Too many exceptions after attempt %s of %s, aborting: %s", attempt, _name, error.getMessage()));
recoveryResult.fail(error);
} else {
// Schedule a new retry task after a computed backoff timeout.
LOGGER.debug(String.format("Attempt %s of %s failed and will be retried after %s millis: %s", attempt, _name, backoffTime, error.getMessage()));
Task<T> retryTask = wrap(attempt);
Promises.propagateResult(retryTask, recoveryResult);
recoveryContext.createTimer(backoffTime, TimeUnit.MILLISECONDS, retryTask);
}
}
/** Starts a retriable task */
private Promise<? extends T> run(Context context) {
_startedAt = System.currentTimeMillis();
Task<T> task = wrap(0);
context.run(task);
return task;
}
}