diff --git a/src/main/java/com/github/rholder/retry/WaitStrategies.java b/src/main/java/com/github/rholder/retry/WaitStrategies.java index fb2c5da..0d44d80 100644 --- a/src/main/java/com/github/rholder/retry/WaitStrategies.java +++ b/src/main/java/com/github/rholder/retry/WaitStrategies.java @@ -157,6 +157,90 @@ public static WaitStrategy exponentialWait(long multiplier, return new ExponentialWaitStrategy(multiplier, maximumTimeUnit.toMillis(maximumTime)); } + /** + * Returns a strategy which sleeps for a random amount of time between the minimumTime and an + * exponentially increasing maximum time. The maximum time increases exponentially after each + * failed attempt up to {@code Long.MAX_VALUE}. + * + * @return a wait strategy that waits a random amount of time and increments + * the maximum bound with each failed attempt using exponential backoff + */ + public static WaitStrategy randomExponentialWait() { + return new RandomExponentialWaitStrategy(1, 0L, 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 waits a random amount of time and increments + * the maximum bound with each failed attempt using exponential backoff + * @throws IllegalStateException if the minimum sleep time is < 0. + */ + public static WaitStrategy randomExponentialWait(long maximumTime, + @Nonnull TimeUnit maximumTimeUnit) { + Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null"); + return new RandomExponentialWaitStrategy(1, 0L, maximumTimeUnit.toMillis(maximumTime)); + } + + /** + * Returns a strategy which sleeps for a random amount of time between the minimumTime and an + * exponentially increasing maximum time. The maximum time increases exponentially after each + * failed attempt up to the maximumTime. + * + * @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 that waits a random amount of time and increments + * the maximum bound with each failed attempt using exponential backoff + * @throws IllegalStateException if the minimum sleep time is < 0, or if the + * maximum sleep time is <= the minimum. + */ + public static WaitStrategy randomExponentialWait(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 RandomExponentialWaitStrategy(1, + minimumTimeUnit.toMillis(minimumTime), + maximumTimeUnit.toMillis(maximumTime)); + } + + /** + * Returns a strategy which sleeps for a random amount of time between the minimumTime and an + * exponentially increasing maximum time. The maximum time increases exponentially after each + * failed attempt up to the maximumTime. + * The resulting wait time can be further controlled by the multiplier + * (but is still held within the minimum and maximum time bounds) + * nextWaitTime = randomExponentialIncrement * {@code multiplier} + * + * @param multiplier multiply the wait time calculated by this + * @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 that waits a random amount of time and increments + * the maximum bound with each failed attempt using exponential backoff + * @throws IllegalStateException if the minimum sleep time is < 0, or if the + * maximum sleep time is <= the minimum, or if the + * multiplier is > the maximum sleep time. + */ + public static WaitStrategy randomExponentialWait(long multiplier, + 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 RandomExponentialWaitStrategy(multiplier, + minimumTimeUnit.toMillis(minimumTime), + 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}. @@ -309,6 +393,40 @@ public long computeSleepTime(Attempt failedAttempt) { } } + @Immutable + private static final class RandomExponentialWaitStrategy implements WaitStrategy { + private static final Random RANDOM = new Random(); + private final long multiplier; + private final long minimumWait; + private final long maximumWait; + + public RandomExponentialWaitStrategy(long multiplier, + long minimumWait, + long maximumWait) { + Preconditions.checkArgument(multiplier > 0L, "multiplier must be > 0 but is %d", multiplier); + Preconditions.checkArgument(minimumWait >= 0L, "minimumWait must be >= 0 but is %d", minimumWait); + Preconditions.checkArgument(maximumWait > minimumWait, "maximumWait must be > minimumWait but is %d", maximumWait); + Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", multiplier); + this.multiplier = multiplier; + this.minimumWait = minimumWait; + this.maximumWait = maximumWait; + } + + @Override + public long computeSleepTime(Attempt failedAttempt) { + long upperBound = Math.round(Math.pow(2, failedAttempt.getAttemptNumber())); + long time = Math.abs(RANDOM.nextLong()) % upperBound; + long result = Math.round(multiplier * time); + if (result < minimumWait) { + result = minimumWait; + } + if (result > maximumWait) { + result = maximumWait; + } + return result >= 0L ? result : 0L; + } + } + @Immutable private static final class FibonacciWaitStrategy implements WaitStrategy { private final long multiplier; diff --git a/src/test/java/com/github/rholder/retry/WaitStrategiesTest.java b/src/test/java/com/github/rholder/retry/WaitStrategiesTest.java index c7d8d4f..aa963fc 100644 --- a/src/test/java/com/github/rholder/retry/WaitStrategiesTest.java +++ b/src/test/java/com/github/rholder/retry/WaitStrategiesTest.java @@ -17,6 +17,7 @@ package com.github.rholder.retry; import com.google.common.base.Function; +import com.google.common.collect.Range; import com.google.common.collect.Sets; import org.junit.Test; @@ -116,6 +117,68 @@ public void testExponentialWithMultiplierAndMaximumWait() { assertTrue(exponentialWait.computeSleepTime(failedAttempt(Integer.MAX_VALUE, 0)) == 50000); } + @Test + public void testRandomExponential() { + WaitStrategy randomExponentialWait = WaitStrategies.randomExponentialWait(); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(1, 0)), 0, 2); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(2, 0)), 0, 4); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(3, 0)), 0, 8); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(4, 0)), 0, 16); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(5, 0)), 0, 32); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(6, 0)), 0, 64); + } + + @Test + public void testRandomExponentialWithMaximumWait() { + WaitStrategy randomExponentialWait = WaitStrategies.randomExponentialWait(40, TimeUnit.MILLISECONDS); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(1, 0)), 0, 2); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(2, 0)), 0, 4); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(3, 0)), 0, 8); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(4, 0)), 0, 16); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(5, 0)), 0, 32); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(6, 0)), 0, 40); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(7, 0)), 0, 40); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(Integer.MAX_VALUE, 0)), 0, 40); + } + + @Test + public void testRandomExponentialWithMinimumAndMaximumWait() { + WaitStrategy randomExponentialWait = WaitStrategies.randomExponentialWait(10, TimeUnit.MILLISECONDS, 40, TimeUnit.MILLISECONDS); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(1, 0)), 10, 10); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(2, 0)), 10, 10); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(3, 0)), 10, 10); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(4, 0)), 10, 16); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(5, 0)), 10, 32); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(6, 0)), 10, 40); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(7, 0)), 10, 40); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(Integer.MAX_VALUE, 0)), 10, 40); + } + + @Test + public void testRandomExponentialWithMultiplierAndMinimumAndMaximumWait() { + WaitStrategy randomExponentialWait = WaitStrategies.randomExponentialWait(1000, 4, TimeUnit.MILLISECONDS, 50000, TimeUnit.MILLISECONDS); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(1, 0)), 4, 2000); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(2, 0)), 4, 4000); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(3, 0)), 4, 8000); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(4, 0)), 4, 16000); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(5, 0)), 4, 32000); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(6, 0)), 4, 50000); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(7, 0)), 4, 50000); + assertWithin(randomExponentialWait.computeSleepTime(failedAttempt(Integer.MAX_VALUE, 0)), 4, 50000); + } + + @Test + public void testRandomExponentialRandomness() { + WaitStrategy randomExponentialWait = WaitStrategies.randomExponentialWait(); + Set times = Sets.newHashSet(); + for (int i = 0; i < 16; i++) { + long time = randomExponentialWait.computeSleepTime(failedAttempt(6, 0)); + assertWithin(time, 0, 64); + times.add(time); + } + assertTrue(times.size() > 1); // if not, the random may not be random + } + @Test public void testFibonacci() { WaitStrategy fibonacciWait = WaitStrategies.fibonacciWait(); @@ -210,4 +273,9 @@ public long getRetryAfter() { return retryAfter; } } + + private static void assertWithin(long value, long min, long max) { + Range range = Range.closed(min, max); + assertTrue(String.format("Expected %s to fall within %s", value, range), range.contains(value)); + } }