/*
* Copyright 2012-2015 Ray Holder
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.hao0.antares.common.retry;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* A retryer, which executes a call, and retries it until it succeeds, or
* a stop strategy decides to stop retrying. A wait strategy is used to sleep
* between attempts. The strategy to decide if the call succeeds or not is
* also configurable.
* <p></p>
* A retryer can also wrap the callable into a RetryerCallable, which can be submitted to an executor.
* <p></p>
* Retryer instances are better constructed with a {@link RetryerBuilder}. A retryer
* is thread-safe, provided the arguments passed to its constructor are thread-safe.
*
* @param <V> the type of the call return value
* @author JB
* @author Jason Dunkelberger (dirkraft)
*/
public final class Retryer<V> {
private final StopStrategy stopStrategy;
private final WaitStrategy waitStrategy;
private final BlockStrategy blockStrategy;
private final AttemptTimeLimiter<V> attemptTimeLimiter;
private final Predicate<Attempt<V>> rejectionPredicate;
private final Collection<RetryListener> listeners;
/**
* Constructor
*
* @param stopStrategy the strategy used to decide when the retryer must stop retrying
* @param waitStrategy the strategy used to decide how much time to sleep between attempts
* @param rejectionPredicate the predicate used to decide if the attempt must be rejected
* or not. If an attempt is rejected, the retryer will retry the call, unless the stop
* strategy indicates otherwise or the thread is interrupted.
*/
public Retryer(@Nonnull StopStrategy stopStrategy,
@Nonnull WaitStrategy waitStrategy,
@Nonnull Predicate<Attempt<V>> rejectionPredicate) {
this(AttemptTimeLimiters.<V>noTimeLimit(), stopStrategy, waitStrategy, BlockStrategies.threadSleepStrategy(), rejectionPredicate);
}
/**
* Constructor
*
* @param attemptTimeLimiter to prevent from any single attempt from spinning infinitely
* @param stopStrategy the strategy used to decide when the retryer must stop retrying
* @param waitStrategy the strategy used to decide how much time to sleep between attempts
* @param rejectionPredicate the predicate used to decide if the attempt must be rejected
* or not. If an attempt is rejected, the retryer will retry the call, unless the stop
* strategy indicates otherwise or the thread is interrupted.
*/
public Retryer(@Nonnull AttemptTimeLimiter<V> attemptTimeLimiter,
@Nonnull StopStrategy stopStrategy,
@Nonnull WaitStrategy waitStrategy,
@Nonnull Predicate<Attempt<V>> rejectionPredicate) {
this(attemptTimeLimiter, stopStrategy, waitStrategy, BlockStrategies.threadSleepStrategy(), rejectionPredicate);
}
/**
* Constructor
*
* @param attemptTimeLimiter to prevent from any single attempt from spinning infinitely
* @param stopStrategy the strategy used to decide when the retryer must stop retrying
* @param waitStrategy the strategy used to decide how much time to sleep between attempts
* @param blockStrategy the strategy used to decide how to block between retry attempts; eg, Thread#sleep(), latches, etc.
* @param rejectionPredicate the predicate used to decide if the attempt must be rejected
* or not. If an attempt is rejected, the retryer will retry the call, unless the stop
* strategy indicates otherwise or the thread is interrupted.
*/
public Retryer(@Nonnull AttemptTimeLimiter<V> attemptTimeLimiter,
@Nonnull StopStrategy stopStrategy,
@Nonnull WaitStrategy waitStrategy,
@Nonnull BlockStrategy blockStrategy,
@Nonnull Predicate<Attempt<V>> rejectionPredicate) {
this(attemptTimeLimiter, stopStrategy, waitStrategy, blockStrategy, rejectionPredicate, new ArrayList<RetryListener>());
}
/**
* Constructor
*
* @param attemptTimeLimiter to prevent from any single attempt from spinning infinitely
* @param stopStrategy the strategy used to decide when the retryer must stop retrying
* @param waitStrategy the strategy used to decide how much time to sleep between attempts
* @param blockStrategy the strategy used to decide how to block between retry attempts; eg, Thread#sleep(), latches, etc.
* @param rejectionPredicate the predicate used to decide if the attempt must be rejected
* or not. If an attempt is rejected, the retryer will retry the call, unless the stop
* strategy indicates otherwise or the thread is interrupted.
* @param listeners collection of retry listeners
*/
@Beta
public Retryer(@Nonnull AttemptTimeLimiter<V> attemptTimeLimiter,
@Nonnull StopStrategy stopStrategy,
@Nonnull WaitStrategy waitStrategy,
@Nonnull BlockStrategy blockStrategy,
@Nonnull Predicate<Attempt<V>> rejectionPredicate,
@Nonnull Collection<RetryListener> listeners) {
Preconditions.checkNotNull(attemptTimeLimiter, "timeLimiter may not be null");
Preconditions.checkNotNull(stopStrategy, "stopStrategy may not be null");
Preconditions.checkNotNull(waitStrategy, "waitStrategy may not be null");
Preconditions.checkNotNull(blockStrategy, "blockStrategy may not be null");
Preconditions.checkNotNull(rejectionPredicate, "rejectionPredicate may not be null");
Preconditions.checkNotNull(listeners, "listeners may not null");
this.attemptTimeLimiter = attemptTimeLimiter;
this.stopStrategy = stopStrategy;
this.waitStrategy = waitStrategy;
this.blockStrategy = blockStrategy;
this.rejectionPredicate = rejectionPredicate;
this.listeners = listeners;
}
/**
* Executes the given callable. If the rejection predicate
* accepts the attempt, the stop strategy is used to decide if a new attempt
* must be made. Then the wait strategy is used to decide how much time to sleep
* and a new attempt is made.
*
* @param callable the callable task to be executed
* @return the computed result of the given callable
* @throws ExecutionException if the given callable throws an exception, and the
* rejection predicate considers the attempt as successful. The original exception
* is wrapped into an ExecutionException.
* @throws RetryException if all the attempts failed before the stop strategy decided
* to abort, or the thread was interrupted. Note that if the thread is interrupted,
* this exception is thrown and the thread's interrupt status is set.
*/
public V call(Callable<V> callable) throws ExecutionException, RetryException {
long startTime = System.nanoTime();
for (int attemptNumber = 1; ; attemptNumber++) {
Attempt<V> attempt;
try {
V result = attemptTimeLimiter.call(callable);
attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
} catch (Throwable t) {
attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}
for (RetryListener listener : listeners) {
listener.onRetry(attempt);
}
if (!rejectionPredicate.apply(attempt)) {
return attempt.get();
}
if (stopStrategy.shouldStop(attempt)) {
throw new RetryException(attemptNumber, attempt);
} else {
long sleepTime = waitStrategy.computeSleepTime(attempt);
try {
blockStrategy.block(sleepTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RetryException(attemptNumber, attempt);
}
}
}
}
/**
* Wraps the given {@link Callable} in a {@link RetryerCallable}, which can
* be submitted to an executor. The returned {@link RetryerCallable} uses
* this {@link Retryer} instance to call the given {@link Callable}.
*
* @param callable the callable to wrap
* @return a {@link RetryerCallable} that behaves like the given {@link Callable} with retry behavior defined by this {@link Retryer}
*/
public RetryerCallable<V> wrap(Callable<V> callable) {
return new RetryerCallable<V>(this, callable);
}
@Immutable
static final class ResultAttempt<R> implements Attempt<R> {
private final R result;
private final long attemptNumber;
private final long delaySinceFirstAttempt;
public ResultAttempt(R result, long attemptNumber, long delaySinceFirstAttempt) {
this.result = result;
this.attemptNumber = attemptNumber;
this.delaySinceFirstAttempt = delaySinceFirstAttempt;
}
@Override
public R get() throws ExecutionException {
return result;
}
@Override
public boolean hasResult() {
return true;
}
@Override
public boolean hasException() {
return false;
}
@Override
public R getResult() throws IllegalStateException {
return result;
}
@Override
public Throwable getExceptionCause() throws IllegalStateException {
throw new IllegalStateException("The attempt resulted in a result, not in an exception");
}
@Override
public long getAttemptNumber() {
return attemptNumber;
}
@Override
public long getDelaySinceFirstAttempt() {
return delaySinceFirstAttempt;
}
}
@Immutable
static final class ExceptionAttempt<R> implements Attempt<R> {
private final ExecutionException e;
private final long attemptNumber;
private final long delaySinceFirstAttempt;
public ExceptionAttempt(Throwable cause, long attemptNumber, long delaySinceFirstAttempt) {
this.e = new ExecutionException(cause);
this.attemptNumber = attemptNumber;
this.delaySinceFirstAttempt = delaySinceFirstAttempt;
}
@Override
public R get() throws ExecutionException {
throw e;
}
@Override
public boolean hasResult() {
return false;
}
@Override
public boolean hasException() {
return true;
}
@Override
public R getResult() throws IllegalStateException {
throw new IllegalStateException("The attempt resulted in an exception, not in a result");
}
@Override
public Throwable getExceptionCause() throws IllegalStateException {
return e.getCause();
}
@Override
public long getAttemptNumber() {
return attemptNumber;
}
@Override
public long getDelaySinceFirstAttempt() {
return delaySinceFirstAttempt;
}
}
/**
* A {@link Callable} which wraps another {@link Callable} in order to add
* retrying behavior from a given {@link Retryer} instance.
*
* @author JB
*/
public static class RetryerCallable<X> implements Callable<X> {
private Retryer<X> retryer;
private Callable<X> callable;
private RetryerCallable(Retryer<X> retryer,
Callable<X> callable) {
this.retryer = retryer;
this.callable = callable;
}
/**
* Makes the enclosing retryer call the wrapped callable.
*
* @see Retryer#call(Callable)
*/
@Override
public X call() throws ExecutionException, RetryException {
return retryer.call(callable);
}
}
}