/*
* 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.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* Factory class for instances of {@link WaitStrategy}.
*
* @author JB
*/
public final class WaitStrategies {
private static final WaitStrategy NO_WAIT_STRATEGY = new FixedWaitStrategy(0L);
private WaitStrategies() {
}
/**
* Returns a wait strategy that doesn't sleep at all before retrying. Use this at your own risk.
*
* @return a wait strategy that doesn't wait between retries
*/
public static WaitStrategy noWait() {
return NO_WAIT_STRATEGY;
}
/**
* Returns a wait strategy that sleeps a fixed amount of time before retrying.
*
* @param sleepTime the time to sleep
* @param timeUnit the unit of the time to sleep
* @return a wait strategy that sleeps a fixed amount of time
* @throws IllegalStateException if the sleep time is < 0
*/
public static WaitStrategy fixedWait(long sleepTime, @Nonnull TimeUnit timeUnit) throws IllegalStateException {
Preconditions.checkNotNull(timeUnit, "The time unit may not be null");
return new FixedWaitStrategy(timeUnit.toMillis(sleepTime));
}
/**
* Returns a strategy that sleeps a random amount of time before retrying.
*
* @param maximumTime the maximum time to sleep
* @param timeUnit the unit of the maximum time
* @return a wait strategy with a random wait time
* @throws IllegalStateException if the maximum sleep time is <= 0.
*/
public static WaitStrategy randomWait(long maximumTime, @Nonnull TimeUnit timeUnit) {
Preconditions.checkNotNull(timeUnit, "The time unit may not be null");
return new RandomWaitStrategy(0L, timeUnit.toMillis(maximumTime));
}
/**
* Returns a strategy that sleeps a random amount of time before retrying.
*
* @param minimumTime the minimum time to sleep
* @param minimumTimeUnit the unit of the minimum time
* @param maximumTime the maximum time to sleep
* @param maximumTimeUnit the unit of the maximum time
* @return a wait strategy with a random wait time
* @throws IllegalStateException if the minimum sleep time is < 0, or if the
* maximum sleep time is less than (or equals to) the minimum.
*/
public static WaitStrategy randomWait(long minimumTime,
@Nonnull TimeUnit minimumTimeUnit,
long maximumTime,
@Nonnull TimeUnit maximumTimeUnit) {
Preconditions.checkNotNull(minimumTimeUnit, "The minimum time unit may not be null");
Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
return new RandomWaitStrategy(minimumTimeUnit.toMillis(minimumTime),
maximumTimeUnit.toMillis(maximumTime));
}
/**
* Returns a strategy that sleeps a fixed amount of time after the first
* failed attempt and in incrementing amounts of time after each additional
* failed attempt.
*
* @param initialSleepTime the time to sleep before retrying the first time
* @param initialSleepTimeUnit the unit of the initial sleep time
* @param increment the increment added to the previous sleep time after each failed attempt
* @param incrementTimeUnit the unit of the increment
* @return a wait strategy that incrementally sleeps an additional fixed time after each failed attempt
*/
public static WaitStrategy incrementingWait(long initialSleepTime,
@Nonnull TimeUnit initialSleepTimeUnit,
long increment,
@Nonnull TimeUnit incrementTimeUnit) {
Preconditions.checkNotNull(initialSleepTimeUnit, "The initial sleep time unit may not be null");
Preconditions.checkNotNull(incrementTimeUnit, "The increment time unit may not be null");
return new IncrementingWaitStrategy(initialSleepTimeUnit.toMillis(initialSleepTime),
incrementTimeUnit.toMillis(increment));
}
/**
* Returns a strategy which sleeps for an exponential amount of time after the first failed attempt,
* and in exponentially incrementing amounts after each failed attempt up to Long.MAX_VALUE.
*
* @return a wait strategy that increments with each failed attempt using exponential backoff
*/
public static WaitStrategy exponentialWait() {
return new ExponentialWaitStrategy(1, Long.MAX_VALUE);
}
/**
* Returns a strategy which sleeps for an exponential amount of time after the first failed attempt,
* and in exponentially incrementing amounts after each failed attempt up to the maximumTime.
*
* @param maximumTime the maximum time to sleep
* @param maximumTimeUnit the unit of the maximum time
* @return a wait strategy that increments with each failed attempt using exponential backoff
*/
public static WaitStrategy exponentialWait(long maximumTime,
@Nonnull TimeUnit maximumTimeUnit) {
Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
return new ExponentialWaitStrategy(1, maximumTimeUnit.toMillis(maximumTime));
}
/**
* Returns a strategy which sleeps for an exponential amount of time after the first failed attempt,
* and in exponentially incrementing amounts after each failed attempt up to the maximumTime.
* The wait time between the retries can be controlled by the multiplier.
* nextWaitTime = exponentialIncrement * {@code multiplier}.
*
* @param multiplier multiply the wait time calculated by this
* @param maximumTime the maximum time to sleep
* @param maximumTimeUnit the unit of the maximum time
* @return a wait strategy that increments with each failed attempt using exponential backoff
*/
public static WaitStrategy exponentialWait(long multiplier,
long maximumTime,
@Nonnull TimeUnit maximumTimeUnit) {
Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
return new ExponentialWaitStrategy(multiplier, maximumTimeUnit.toMillis(maximumTime));
}
/**
* Returns a strategy which sleeps for an increasing amount of time after the first failed attempt,
* and in Fibonacci increments after each failed attempt up to {@link Long#MAX_VALUE}.
*
* @return a wait strategy that increments with each failed attempt using a Fibonacci sequence
*/
public static WaitStrategy fibonacciWait() {
return new FibonacciWaitStrategy(1, Long.MAX_VALUE);
}
/**
* Returns a strategy which sleeps for an increasing amount of time after the first failed attempt,
* and in Fibonacci increments after each failed attempt up to the {@code maximumTime}.
*
* @param maximumTime the maximum time to sleep
* @param maximumTimeUnit the unit of the maximum time
* @return a wait strategy that increments with each failed attempt using a Fibonacci sequence
*/
public static WaitStrategy fibonacciWait(long maximumTime,
@Nonnull TimeUnit maximumTimeUnit) {
Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
return new FibonacciWaitStrategy(1, maximumTimeUnit.toMillis(maximumTime));
}
/**
* Returns a strategy which sleeps for an increasing amount of time after the first failed attempt,
* and in Fibonacci increments after each failed attempt up to the {@code maximumTime}.
* The wait time between the retries can be controlled by the multiplier.
* nextWaitTime = fibonacciIncrement * {@code multiplier}.
*
* @param multiplier multiply the wait time calculated by this
* @param maximumTime the maximum time to sleep
* @param maximumTimeUnit the unit of the maximum time
* @return a wait strategy that increments with each failed attempt using a Fibonacci sequence
*/
public static WaitStrategy fibonacciWait(long multiplier,
long maximumTime,
@Nonnull TimeUnit maximumTimeUnit) {
Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
return new FibonacciWaitStrategy(multiplier, maximumTimeUnit.toMillis(maximumTime));
}
/**
* Returns a strategy which sleeps for an amount of time based on the Exception that occurred. The
* {@code function} determines how the sleep time should be calculated for the given
* {@code exceptionClass}. If the exception does not match, a wait time of 0 is returned.
*
* @param function function to calculate sleep time
* @param exceptionClass class to calculate sleep time from
* @return a wait strategy calculated from the failed attempt
*/
public static <T extends Throwable> WaitStrategy exceptionWait(@Nonnull Class<T> exceptionClass,
@Nonnull Function<T, Long> function) {
Preconditions.checkNotNull(exceptionClass, "exceptionClass may not be null");
Preconditions.checkNotNull(function, "function may not be null");
return new ExceptionWaitStrategy<T>(exceptionClass, function);
}
/**
* Joins one or more wait strategies to derive a composite wait strategy.
* The new joined strategy will have a wait time which is total of all wait times computed one after another in order.
*
* @param waitStrategies Wait strategies that need to be applied one after another for computing the sleep time.
* @return A composite wait strategy
*/
public static WaitStrategy join(WaitStrategy... waitStrategies) {
Preconditions.checkState(waitStrategies.length > 0, "Must have at least one wait strategy");
List<WaitStrategy> waitStrategyList = Lists.newArrayList(waitStrategies);
Preconditions.checkState(!waitStrategyList.contains(null), "Cannot have a null wait strategy");
return new CompositeWaitStrategy(waitStrategyList);
}
@Immutable
private static final class FixedWaitStrategy implements WaitStrategy {
private final long sleepTime;
public FixedWaitStrategy(long sleepTime) {
Preconditions.checkArgument(sleepTime >= 0L, "sleepTime must be >= 0 but is %d", sleepTime);
this.sleepTime = sleepTime;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
return sleepTime;
}
}
@Immutable
private static final class RandomWaitStrategy implements WaitStrategy {
private static final Random RANDOM = new Random();
private final long minimum;
private final long maximum;
public RandomWaitStrategy(long minimum, long maximum) {
Preconditions.checkArgument(minimum >= 0, "minimum must be >= 0 but is %d", minimum);
Preconditions.checkArgument(maximum > minimum, "maximum must be > minimum but maximum is %d and minimum is", maximum, minimum);
this.minimum = minimum;
this.maximum = maximum;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
long t = Math.abs(RANDOM.nextLong()) % (maximum - minimum);
return t + minimum;
}
}
@Immutable
private static final class IncrementingWaitStrategy implements WaitStrategy {
private final long initialSleepTime;
private final long increment;
public IncrementingWaitStrategy(long initialSleepTime,
long increment) {
Preconditions.checkArgument(initialSleepTime >= 0L, "initialSleepTime must be >= 0 but is %d", initialSleepTime);
this.initialSleepTime = initialSleepTime;
this.increment = increment;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
long result = initialSleepTime + (increment * (failedAttempt.getAttemptNumber() - 1));
return result >= 0L ? result : 0L;
}
}
@Immutable
private static final class ExponentialWaitStrategy implements WaitStrategy {
private final long multiplier;
private final long maximumWait;
public ExponentialWaitStrategy(long multiplier,
long maximumWait) {
Preconditions.checkArgument(multiplier > 0L, "multiplier must be > 0 but is %d", multiplier);
Preconditions.checkArgument(maximumWait >= 0L, "maximumWait must be >= 0 but is %d", maximumWait);
Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", multiplier);
this.multiplier = multiplier;
this.maximumWait = maximumWait;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
double exp = Math.pow(2, failedAttempt.getAttemptNumber());
long result = Math.round(multiplier * exp);
if (result > maximumWait) {
result = maximumWait;
}
return result >= 0L ? result : 0L;
}
}
@Immutable
private static final class FibonacciWaitStrategy implements WaitStrategy {
private final long multiplier;
private final long maximumWait;
public FibonacciWaitStrategy(long multiplier, long maximumWait) {
Preconditions.checkArgument(multiplier > 0L, "multiplier must be > 0 but is %d", multiplier);
Preconditions.checkArgument(maximumWait >= 0L, "maximumWait must be >= 0 but is %d", maximumWait);
Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", multiplier);
this.multiplier = multiplier;
this.maximumWait = maximumWait;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
long fib = fib(failedAttempt.getAttemptNumber());
long result = multiplier * fib;
if (result > maximumWait || result < 0L) {
result = maximumWait;
}
return result >= 0L ? result : 0L;
}
private long fib(long n) {
if (n == 0L) return 0L;
if (n == 1L) return 1L;
long prevPrev = 0L;
long prev = 1L;
long result = 0L;
for (long i = 2L; i <= n; i++) {
result = prev + prevPrev;
prevPrev = prev;
prev = result;
}
return result;
}
}
@Immutable
private static final class CompositeWaitStrategy implements WaitStrategy {
private final List<WaitStrategy> waitStrategies;
public CompositeWaitStrategy(List<WaitStrategy> waitStrategies) {
Preconditions.checkState(!waitStrategies.isEmpty(), "Need at least one wait strategy");
this.waitStrategies = waitStrategies;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
long waitTime = 0L;
for (WaitStrategy waitStrategy : waitStrategies) {
waitTime += waitStrategy.computeSleepTime(failedAttempt);
}
return waitTime;
}
}
@Immutable
private static final class ExceptionWaitStrategy<T extends Throwable> implements WaitStrategy {
private final Class<T> exceptionClass;
private final Function<T, Long> function;
public ExceptionWaitStrategy(@Nonnull Class<T> exceptionClass, @Nonnull Function<T, Long> function) {
this.exceptionClass = exceptionClass;
this.function = function;
}
@SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "ConstantConditions", "unchecked"})
@Override
public long computeSleepTime(Attempt lastAttempt) {
if (lastAttempt.hasException()) {
Throwable cause = lastAttempt.getExceptionCause();
if (exceptionClass.isAssignableFrom(cause.getClass())) {
return function.apply((T) cause);
}
}
return 0L;
}
}
}