All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fr.free.jnizet.retry.Retryer Maven / Gradle / Ivy

Go to download

This is a small extension to Google's Guava library to allow for the creation of configurable retrying strategies for an arbitrary function call, such as something that talks to a remote service with flaky uptime.

There is a newer version: 2.0.0
Show newest version
package fr.free.jnizet.retry;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;

/**
 * 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 * @param the type of the call return value */ public final class Retryer { private final StopStrategy stopStrategy; private final WaitStrategy waitStrategy; 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) { 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.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 = callable.call(); 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); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy