Skip to content

Commit

Permalink
Added jitter to retry policies.
Browse files Browse the repository at this point in the history
  • Loading branch information
jhalterman committed Jul 26, 2016
1 parent 9057325 commit 894632c
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 37 deletions.
24 changes: 21 additions & 3 deletions README.md
Expand Up @@ -84,21 +84,33 @@ It can add a fixed delay between retries:
retryPolicy.withDelay(1, TimeUnit.SECONDS);
```

Or a delay that backs off exponentially:
Or a delay that [backs off][backoff] exponentially:

```java
retryPolicy.withBackoff(1, 30, TimeUnit.SECONDS);
```

It can add a max number of retries and a max retry duration:
It can add a random [jitter factor][jitter-factor] to the delay:

```java
retryPolicy.withJitter(.1);
```

Or a [time based jitter][jitter-duration]:

```java
retryPolicy.withJitter(100, TimeUnit.MILLISECONDS);
```

It can add a [max number of retries][max-retries] and a [max retry duration][max-duration]:

```java
retryPolicy
.withMaxRetries(100)
.withMaxDuration(5, TimeUnit.MINUTES);
```

It can also specify which results, failures or conditions to abort retries on:
It can also specify which results, failures or conditions to [abort retries][abort-retries] on:

```java
retryPolicy
Expand Down Expand Up @@ -431,6 +443,12 @@ Failsafe is a volunteer effort. If you use it and you like it, you can help by s

Copyright 2015-2016 Jonathan Halterman and friends. Released under the [Apache 2.0 license](http://www.apache.org/licenses/LICENSE-2.0.html).

[backoff]: http://jodah.net/failsafe/javadoc/net/jodah/failsafe/RetryPolicy.html#withBackoff-long-long-java.util.concurrent.TimeUnit-
[abort-retries]: http://jodah.net/failsafe/javadoc/net/jodah/failsafe/RetryPolicy.html#abortOn-java.lang.Class...-
[max-retries]: http://jodah.net/failsafe/javadoc/net/jodah/failsafe/RetryPolicy.html#withMaxRetries-int-
[max-duration]: http://jodah.net/failsafe/javadoc/net/jodah/failsafe/RetryPolicy.html#withMaxRetries-int-
[jitter-duration]: http://jodah.net/failsafe/javadoc/net/jodah/failsafe/RetryPolicy.html#withJitter-long-java.util.concurrent.TimeUnit-
[jitter-factor]: http://jodah.net/failsafe/javadoc/net/jodah/failsafe/RetryPolicy.html#withJitter-double-
[Listeners]: http://jodah.net/failsafe/javadoc/net/jodah/failsafe/Listeners.html
[ListenerConfig]: http://jodah.net/failsafe/javadoc/net/jodah/failsafe/ListenerConfig.html
[AsyncListenerConfig]: http://jodah.net/failsafe/javadoc/net/jodah/failsafe/AsyncListenerConfig.html
Expand Down
29 changes: 24 additions & 5 deletions src/main/java/net/jodah/failsafe/AbstractExecution.java
Expand Up @@ -17,6 +17,7 @@ abstract class AbstractExecution extends ExecutionContext {
volatile boolean completed;
volatile boolean retriesExceeded;
volatile boolean success;
volatile long delayNanos;
volatile long waitNanos;

/**
Expand All @@ -29,7 +30,7 @@ abstract class AbstractExecution extends ExecutionContext {
this.retryPolicy = retryPolicy;
this.circuitBreaker = circuitBreaker;
this.listeners = listeners;
waitNanos = retryPolicy.getDelay().toNanos();
waitNanos = delayNanos = retryPolicy.getDelay().toNanos();
}

/**
Expand Down Expand Up @@ -90,6 +91,18 @@ boolean complete(Object result, Throwable failure, boolean checkArgs) {
circuitBreaker.recordSuccess();
}

// Adjust the delay for backoffs
if (retryPolicy.getMaxDelay() != null)
delayNanos = (long) Math.min(delayNanos * retryPolicy.getDelayFactor(), retryPolicy.getMaxDelay().toNanos());

// Calculate the wait time with jitter
if (retryPolicy.getJitter() != null)
waitNanos = randomizeDelay(delayNanos, retryPolicy.getJitter().toNanos(), Math.random());
else if (retryPolicy.getJitterFactor() > 0.0)
waitNanos = randomizeDelay(delayNanos, retryPolicy.getJitterFactor(), Math.random());
else
waitNanos = delayNanos;

// Adjust the wait time for max duration
if (retryPolicy.getMaxDuration() != null) {
long maxRemainingWaitTime = retryPolicy.getMaxDuration().toNanos() - elapsedNanos;
Expand All @@ -98,10 +111,6 @@ boolean complete(Object result, Throwable failure, boolean checkArgs) {
waitNanos = 0;
}

// Adjust the wait time for backoffs
if (retryPolicy.getMaxDelay() != null)
waitNanos = (long) Math.min(waitNanos * retryPolicy.getDelayMultiplier(), retryPolicy.getMaxDelay().toNanos());

boolean maxRetriesExceeded = retryPolicy.getMaxRetries() != -1 && executions > retryPolicy.getMaxRetries();
boolean maxDurationExceeded = retryPolicy.getMaxDuration() != null
&& elapsedNanos > retryPolicy.getMaxDuration().toNanos();
Expand All @@ -128,4 +137,14 @@ boolean complete(Object result, Throwable failure, boolean checkArgs) {

return completed;
}

static long randomizeDelay(long delay, long jitter, double random) {
double randomAddend = (1 - random * 2) * jitter;
return (long) (delay + randomAddend);
}

static long randomizeDelay(long delay, double jitterFactor, double random) {
double randomFactor = 1 + (1 - random * 2) * jitterFactor;
return (long) (delay * randomFactor);
}
}
2 changes: 1 addition & 1 deletion src/main/java/net/jodah/failsafe/AsyncExecution.java
Expand Up @@ -157,7 +157,7 @@ synchronized boolean complete(Object result, Throwable failure, boolean checkArg
synchronized boolean completeOrRetry(Object result, Throwable failure) {
if (!complete(result, failure, true) && !future.isDone() && !future.isCancelled()) {
try {
future.setFuture((Future) scheduler.schedule(callable, waitNanos, TimeUnit.NANOSECONDS));
future.setFuture((Future) scheduler.schedule(callable, delayNanos, TimeUnit.NANOSECONDS));
return true;
} catch (Throwable t) {
failure = t;
Expand Down
120 changes: 94 additions & 26 deletions src/main/java/net/jodah/failsafe/RetryPolicy.java
Expand Up @@ -25,7 +25,9 @@ public class RetryPolicy {
static final RetryPolicy NEVER = new RetryPolicy().withMaxRetries(0);

private Duration delay;
private double delayMultiplier;
private double delayFactor;
private Duration jitter;
private double jitterFactor;
private Duration maxDelay;
private Duration maxDuration;
private int maxRetries;
Expand All @@ -49,10 +51,12 @@ public RetryPolicy() {
*/
public RetryPolicy(RetryPolicy rp) {
this.delay = rp.delay;
this.delayMultiplier = rp.delayMultiplier;
this.delayFactor = rp.delayFactor;
this.maxDelay = rp.maxDelay;
this.maxDuration = rp.maxDuration;
this.maxRetries = rp.maxRetries;
this.jitter = rp.jitter;
this.jitterFactor = rp.jitterFactor;
this.failuresChecked = rp.failuresChecked;
this.retryConditions = new ArrayList<BiPredicate<Object, Throwable>>(rp.retryConditions);
this.abortConditions = new ArrayList<BiPredicate<Object, Throwable>>(rp.abortConditions);
Expand Down Expand Up @@ -196,12 +200,30 @@ public Duration getDelay() {
}

/**
* Returns the delay multiplier for backoff retries.
* Returns the delay factor for backoff retries.
*
* @see #withBackoff(long, long, TimeUnit, double)
*/
public double getDelayMultiplier() {
return delayMultiplier;
public double getDelayFactor() {
return delayFactor;
}

/**
* Returns the jitter, else {@code null} if none has been configured.
*
* @see #withJitter(long, TimeUnit)
*/
public Duration getJitter() {
return jitter;
}

/**
* Returns the jitter factor, else {@code 0.0} is none has been configured.
*
* @see #withJitter(double)
*/
public double getJitterFactor() {
return jitterFactor;
}

/**
Expand Down Expand Up @@ -307,35 +329,38 @@ public RetryPolicy retryWhen(Object result) {
}

/**
* Sets the {@code delay} between retries, exponentially backing off to the {@code maxDelay} and multiplying successive
* delays by a factor of 2.
* Sets the {@code delay} between retries, exponentially backing off to the {@code maxDelay} and multiplying
* successive delays by a factor of 2.
*
* @throws NullPointerException if {@code timeUnit} is null
* @throws IllegalStateException if {@code delay} is >= the {@link RetryPolicy#withMaxDuration(long, TimeUnit)
* maxDuration}
* @throws IllegalArgumentException if {@code delay} is <= 0 or {@code delay} is >= {@code maxDelay}
*/
public RetryPolicy withBackoff(long delay, long maxDelay, TimeUnit timeUnit) {
return withBackoff(delay, maxDelay, timeUnit, 2);
}

/**
* Sets the {@code delay} between retries, exponentially backing off to the {@code maxDelay} and multiplying successive
* delays by the {@code delayMultiplier}.
* Sets the {@code delay} between retries, exponentially backing off to the {@code maxDelay} and multiplying
* successive delays by the {@code delayFactor}.
*
* @throws NullPointerException if {@code timeUnit} is null
* @throws IllegalStateException if {@code delay} is >= the maxDuration
* @throws IllegalStateException if {@code delay} is >= the {@link RetryPolicy#withMaxDuration(long, TimeUnit)
* maxDuration}
* @throws IllegalArgumentException if {@code delay} <= 0, {@code delay} is >= {@code maxDelay}, or the
* {@code delayMultiplier} is <= 1
* {@code delayFactor} is <= 1
*/
public RetryPolicy withBackoff(long delay, long maxDelay, TimeUnit timeUnit, double delayMultiplier) {
public RetryPolicy withBackoff(long delay, long maxDelay, TimeUnit timeUnit, double delayFactor) {
Assert.notNull(timeUnit, "timeUnit");
Assert.isTrue(timeUnit.toNanos(delay) > 0, "The delay must be greater than 0");
Assert.state(maxDuration == null || timeUnit.toNanos(delay) < maxDuration.toNanos(),
"delay must be less than the maxDuration");
Assert.isTrue(timeUnit.toNanos(delay) < timeUnit.toNanos(maxDelay), "delay must be less than the maxDelay");
Assert.isTrue(delayFactor > 1, "delayFactor must be greater than 1");
this.delay = new Duration(delay, timeUnit);
this.maxDelay = new Duration(maxDelay, timeUnit);
this.delayMultiplier = delayMultiplier;
Assert.isTrue(this.delay.toNanos() > 0, "The delay must be greater than 0");
if (maxDuration != null)
Assert.state(this.delay.toNanos() < this.maxDuration.toNanos(), "delay must be less than the maxDuration");
Assert.isTrue(this.delay.toNanos() < this.maxDelay.toNanos(), "delay must be less than the maxDelay");
Assert.isTrue(delayMultiplier > 1, "delayMultiplier must be greater than 1");
this.delayFactor = delayFactor;
return this;
}

Expand All @@ -344,28 +369,71 @@ public RetryPolicy withBackoff(long delay, long maxDelay, TimeUnit timeUnit, dou
*
* @throws NullPointerException if {@code timeUnit} is null
* @throws IllegalArgumentException if {@code delay} <= 0
* @throws IllegalStateException if {@code delay} is >= the maxDuration
* @throws IllegalStateException if {@code delay} is >= the {@link RetryPolicy#withMaxDuration(long, TimeUnit)
* maxDuration}
*/
public RetryPolicy withDelay(long delay, TimeUnit timeUnit) {
Assert.notNull(timeUnit, "timeUnit");
this.delay = new Duration(delay, timeUnit);
Assert.isTrue(this.delay.toNanos() > 0, "delay must be greater than 0");
if (maxDuration != null)
Assert.state(this.delay.toNanos() < maxDuration.toNanos(), "delay must be less than the maxDuration");
Assert.isTrue(timeUnit.toNanos(delay) > 0, "delay must be greater than 0");
Assert.state(maxDuration == null || timeUnit.toNanos(delay) < maxDuration.toNanos(),
"delay must be less than the maxDuration");
Assert.state(maxDelay == null, "Backoff delays have already been set");
this.delay = new Duration(delay, timeUnit);
return this;
}

/**
* Sets the {@code jitter} to randomly vary retry delays by. For each retry delay, a random portion of the
* {@code jitterFactor} multiplied by the delay with be added or subtracted to the delay. For example: a retry delay
* of 100 milliseconds and a {@code jitterFactor} of .25 will result in a random retry delay between 75 and 125
* milliseconds.
* <p>
* Jitter should be combined with {@link #withDelay(long, TimeUnit) fixed} or
* {@link #withBackoff(long, long, TimeUnit) exponential backoff} delays.
*
* @throws IllegalArgumentException if {@code duration} is <= 0 or > 1
* @throws IllegalStateException if no delay has been configured or {@link #withJitter(long, TimeUnit)} has already
* been called
*/
public RetryPolicy withJitter(double jitterFactor) {
Assert.isTrue(jitterFactor > 0 && jitterFactor <= 1, "jitterFactor must be > 0 and <= 1");
Assert.state(delay != null, "A fixed or exponential backoff delay must be configured");
Assert.state(jitter == null, "withJitter(long, timeUnit) has already been called");
this.jitterFactor = jitterFactor;
return this;
}

/**
* Sets the {@code jitter} to randomly vary retry delays by. For each retry delay, a random portion of the
* {@code jitter} will be added or subtracted to the delay. For example: a {@code jitter} of 100 milliseconds will
* randomly add between -100 and 100 milliseconds to each retry delay.
* <p>
* Jitter should be combined with {@link #withDelay(long, TimeUnit) fixed} or
* {@link #withBackoff(long, long, TimeUnit) exponential backoff} delays.
*
* @throws NullPointerException if {@code timeUnit} is null
* @throws IllegalArgumentException if {@code jitter} is <= 0
* @throws IllegalStateException if no delay has been configured or {@link #withJitter(long)} has already been called
*/
public RetryPolicy withJitter(long jitter, TimeUnit timeUnit) {
Assert.notNull(timeUnit, "timeUnit");
Assert.isTrue(jitter > 0, "jitter must be > 0");
Assert.state(delay != null, "A fixed or exponential backoff delay must be configured");
Assert.state(jitterFactor == 0.0, "withJitter(long) has already been called");
this.jitter = new Duration(jitter, timeUnit);
return this;
}

/**
* Sets the max duration to perform retries for.
* Sets the max duration to perform retries for, else the execution will be failed.
*
* @throws NullPointerException if {@code timeUnit} is null
* @throws IllegalStateException if {@code maxDuration} is <= the delay
* @throws IllegalStateException if {@code maxDuration} is <= the {@link RetryPolicy#withDelay(long, TimeUnit) delay}
*/
public RetryPolicy withMaxDuration(long maxDuration, TimeUnit timeUnit) {
Assert.notNull(timeUnit, "timeUnit");
Assert.state(timeUnit.toNanos(maxDuration) > delay.toNanos(), "maxDuration must be greater than the delay");
this.maxDuration = new Duration(maxDuration, timeUnit);
Assert.state(this.maxDuration.toNanos() > delay.toNanos(), "maxDuration must be greater than the delay");
return this;
}

Expand Down
31 changes: 31 additions & 0 deletions src/test/java/net/jodah/failsafe/AbstractExecutionTest.java
@@ -0,0 +1,31 @@
package net.jodah.failsafe;

import static org.testng.Assert.assertEquals;

import org.testng.annotations.Test;

@Test
public class AbstractExecutionTest {
public void testRandomizeDelayForFactor() {
assertEquals(AbstractExecution.randomizeDelay(100, .5, 0), 150);
assertEquals(AbstractExecution.randomizeDelay(100, .5, .25), 125);
assertEquals(AbstractExecution.randomizeDelay(100, .5, .5), 100);
assertEquals(AbstractExecution.randomizeDelay(100, .5, .75), 75);
assertEquals(AbstractExecution.randomizeDelay(100, .5, .9999), 50);

assertEquals(AbstractExecution.randomizeDelay(500, .5, .25), 625);
assertEquals(AbstractExecution.randomizeDelay(500, .5, .75), 375);
assertEquals(AbstractExecution.randomizeDelay(50000, .5, .25), 62500);
}

public void testRandomizeDelayForDuration() {
assertEquals(AbstractExecution.randomizeDelay(100, 50, 0), 150);
assertEquals(AbstractExecution.randomizeDelay(100, 50, .25), 125);
assertEquals(AbstractExecution.randomizeDelay(100, 50, .5), 100);
assertEquals(AbstractExecution.randomizeDelay(100, 50, .75), 75);
assertEquals(AbstractExecution.randomizeDelay(100, 50, .9999), 50);

assertEquals(AbstractExecution.randomizeDelay(500, 50, .25), 525);
assertEquals(AbstractExecution.randomizeDelay(50000, 5000, .25), 52500);
}
}
2 changes: 1 addition & 1 deletion src/test/java/net/jodah/failsafe/RetryPolicyTest.java
Expand Up @@ -174,7 +174,7 @@ public void testCopy() {

RetryPolicy rp2 = rp.copy();
assertEquals(rp.getDelay().toNanos(), rp2.getDelay().toNanos());
assertEquals(rp.getDelayMultiplier(), rp2.getDelayMultiplier());
assertEquals(rp.getDelayFactor(), rp2.getDelayFactor());
assertEquals(rp.getMaxDelay().toNanos(), rp2.getMaxDelay().toNanos());
assertEquals(rp.getMaxDuration().toNanos(), rp2.getMaxDuration().toNanos());
assertEquals(rp.getMaxRetries(), rp2.getMaxRetries());
Expand Down
Expand Up @@ -11,7 +11,7 @@

public class AsyncExample {
static ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
static RetryPolicy retryPolicy = new RetryPolicy().withDelay(100, TimeUnit.MILLISECONDS);
static RetryPolicy retryPolicy = new RetryPolicy().withDelay(100, TimeUnit.MILLISECONDS).withJitter(.25);
static Service service = new Service();

public static class Service {
Expand Down

0 comments on commit 894632c

Please sign in to comment.