com.github.rholder.retry.Retryer Maven / Gradle / Ivy
Show all versions of guava-retrying Show documentation
/*
* Copyright 2012-2013 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 com.github.rholder.retry;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
/**
* 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.
*
* A retryer can also wrap the callable into a RetryerCallable, which can be submitted to an executor.
*
* Retryer instances are better constructed with a {@link RetryerBuilder}. A retryer
* is thread-safe, provided the arguments passed to its constructor are thread-safe.
*
* @author JB
* @author Jason Dunkelberger (dirkraft)
* @param the type of the call return value
*/
public final class Retryer {
private final StopStrategy stopStrategy;
private final WaitStrategy waitStrategy;
private final AttemptTimeLimiter attemptTimeLimiter;
private final Predicate> rejectionPredicate;
/**
* 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> rejectionPredicate) {
this(AttemptTimeLimiters.noTimeLimit(), stopStrategy, waitStrategy, 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 attemptTimeLimiter,
@Nonnull StopStrategy stopStrategy,
@Nonnull WaitStrategy waitStrategy,
@Nonnull Predicate> rejectionPredicate) {
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(rejectionPredicate, "waitStrategy may not be null");
this.attemptTimeLimiter = attemptTimeLimiter;
this.stopStrategy = stopStrategy;
this.waitStrategy = waitStrategy;
this.rejectionPredicate = rejectionPredicate;
}
/**
* 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 must time to sleep,
* and a new attempt is made.
* @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 callable) throws ExecutionException, RetryException {
long startTime = System.currentTimeMillis();
for (int attemptNumber = 1; ; attemptNumber++) {
Attempt attempt;
try {
V result = attemptTimeLimiter.call(callable);
attempt = new ResultAttempt(result);
} catch (Throwable t) {
attempt = new ExceptionAttempt(t);
}
if (!rejectionPredicate.apply(attempt)) {
return attempt.get();
}
long delaySinceFirstAttemptInMillis = System.currentTimeMillis() - startTime;
if (stopStrategy.shouldStop(attemptNumber, delaySinceFirstAttemptInMillis)) {
throw new RetryException(attemptNumber, attempt);
} else {
long sleepTime = waitStrategy.computeSleepTime(attemptNumber, System.currentTimeMillis() - startTime);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RetryException(attemptNumber, attempt);
}
}
}
}
/**
* Wraps the given callable into a {@link RetryerCallable}, which can be submitted to an executor.
* The returned callable will use this retryer to call the given callable
* @param callable the callable to wrap
*/
public RetryerCallable wrap(Callable callable) {
return new RetryerCallable(this, callable);
}
@Immutable
private static final class ResultAttempt implements Attempt {
private final R result;
public ResultAttempt(R result) {
this.result = result;
}
@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");
}
}
@Immutable
private static final class ExceptionAttempt implements Attempt {
private final ExecutionException e;
public ExceptionAttempt(Throwable cause) {
this.e = new ExecutionException(cause);
}
@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();
}
}
/**
* A Callable which wraps another callable in order to make it call by the enclosing retryer.
* @author JB
*/
public static class RetryerCallable implements Callable {
private Retryer retryer;
private Callable callable;
private RetryerCallable(Retryer retryer,
Callable 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);
}
}
}