Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions src/main/java/com/github/rholder/retry/WaitStrategies.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -309,6 +393,40 @@ public long computeSleepTime(Attempt failedAttempt) {
}
}

@Immutable
private static final class RandomExponentialWaitStrategy implements WaitStrategy {
private static final Random RANDOM = new Random();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you used a private static final Random for consistency with the existing code; I wonder if it's worth switching over to a shared ThreadLocalRandom?

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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return Math.max(result, 0)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it this way for consistency with the other code in this file. Is it worth it to replace them all to be consistent?

}
}

@Immutable
private static final class FibonacciWaitStrategy implements WaitStrategy {
private final long multiplier;
Expand Down
68 changes: 68 additions & 0 deletions src/test/java/com/github/rholder/retry/WaitStrategiesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Long> 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();
Expand Down Expand Up @@ -210,4 +273,9 @@ public long getRetryAfter() {
return retryAfter;
}
}

private static void assertWithin(long value, long min, long max) {
Range<Long> range = Range.closed(min, max);
assertTrue(String.format("Expected %s to fall within %s", value, range), range.contains(value));
}
}