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
12 changes: 10 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
<version>0.1.19-SNAPSHOT</version>

<properties>
<more-lambdas.version>0.1.12</more-lambdas.version>
<slf4j-api.version>1.7.14</slf4j-api.version>
<more-lambdas.version>0.1.34</more-lambdas.version>
<slf4j-api.version>1.7.15</slf4j-api.version>
<guava.version>21.0</guava.version>
<jsr305.version>3.0.2</jsr305.version>

<junit.version>5.5.2</junit.version>
<logback-classic.version>1.2.3</logback-classic.version>
<commons-math3.version>3.6.1</commons-math3.version>
<jmh.version>1.21</jmh.version>
<assert.version>3.14.0</assert.version>

<maven-surefire-plugin.version>3.0.0-M3</maven-surefire-plugin.version>
<nexus-staging-maven-plugin.version>1.6.8</nexus-staging-maven-plugin.version>
Expand Down Expand Up @@ -132,6 +133,13 @@
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assert.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.github.phantomthief.failover.backoff;

/**
* Provide a {@link BackOffExecution} that indicates the rate at which
* an operation should be retried.
*
* <p>Users of this interface are expected to use it like this:
*
* <pre class="code">
* BackOffExecution exec = backOff.start();
*
* // In the operation recovery/retry loop:
* long waitInterval = exec.nextBackOff();
* if (waitInterval == BackOffExecution.STOP) {
* // do not retry operation
* }
* else {
* // sleep, e.g. Thread.sleep(waitInterval)
* // retry operation
* }
* }</pre>
*
* Once the underlying operation has completed successfully,
* the execution instance can be simply discarded.
*
* @see BackOffExecution
*/
@FunctionalInterface
public interface BackOff {

/**
* Start a new back off execution.
*
* @return a fresh {@link BackOffExecution} ready to be used
*/
BackOffExecution start();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.github.phantomthief.failover.backoff;

/**
* Represent a particular back-off execution.
*
* <p>Implementations do not need to be thread safe.
*
* @see BackOff
*/
@FunctionalInterface
public interface BackOffExecution {

/**
* Return value of {@link #nextBackOff()} that indicates that the operation
* should not be retried.
*/
long STOP = -1;

/**
* Return the number of milliseconds to wait before retrying the operation
* or {@link #STOP} ({@value #STOP}) to indicate that no further attempt
* should be made for the operation.
*/
long nextBackOff();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package com.github.phantomthief.failover.backoff;

import static com.google.common.base.Preconditions.checkArgument;

/**
* Implementation of {@link BackOff} that increases the back off period for each
* retry attempt. When the interval has reached the {@link #setMaxInterval(long)
* max interval}, it is no longer increased. Stops retrying once the
* {@link #setMaxElapsedTime(long) max elapsed time} has been reached.
*
* <p>Example: The default interval is {@value #DEFAULT_INITIAL_INTERVAL} ms,
* the default multiplier is {@value #DEFAULT_MULTIPLIER}, and the default max
* interval is {@value #DEFAULT_MAX_INTERVAL}. For 10 attempts the sequence will be
* as follows:
*
* <pre>
* request# back off
*
* 1 2000
* 2 3000
* 3 4500
* 4 6750
* 5 10125
* 6 15187
* 7 22780
* 8 30000
* 9 30000
* 10 30000
* </pre>
*
* <p>Note that the default max elapsed time is {@link Long#MAX_VALUE}. Use
* {@link #setMaxElapsedTime(long)} to limit the maximum length of time
* that an instance should accumulate before returning
* {@link BackOffExecution#STOP}.
*/
public class ExponentialBackOff implements BackOff {

/**
* The default initial interval.
*/
public static final long DEFAULT_INITIAL_INTERVAL = 2000L;

/**
* The default multiplier (increases the interval by 50%).
*/
public static final double DEFAULT_MULTIPLIER = 1.5;

/**
* The default maximum back off time.
*/
public static final long DEFAULT_MAX_INTERVAL = 30000L;

/**
* The default maximum elapsed time.
*/
public static final long DEFAULT_MAX_ELAPSED_TIME = Long.MAX_VALUE;


private long initialInterval = DEFAULT_INITIAL_INTERVAL;

private double multiplier = DEFAULT_MULTIPLIER;

private long maxInterval = DEFAULT_MAX_INTERVAL;

private long maxElapsedTime = DEFAULT_MAX_ELAPSED_TIME;


/**
* Create an instance with the default settings.
*
* @see #DEFAULT_INITIAL_INTERVAL
* @see #DEFAULT_MULTIPLIER
* @see #DEFAULT_MAX_INTERVAL
* @see #DEFAULT_MAX_ELAPSED_TIME
*/
public ExponentialBackOff() {
}

/**
* Create an instance with the supplied settings.
*
* @param initialInterval the initial interval in milliseconds
* @param multiplier the multiplier (should be greater than or equal to 1)
*/
public ExponentialBackOff(long initialInterval, double multiplier) {
checkMultiplier(multiplier);
this.initialInterval = initialInterval;
this.multiplier = multiplier;
}

/**
* The initial interval in milliseconds.
*/
public void setInitialInterval(long initialInterval) {
this.initialInterval = initialInterval;
}

/**
* Return the initial interval in milliseconds.
*/
public long getInitialInterval() {
return this.initialInterval;
}

/**
* The value to multiply the current interval by for each retry attempt.
*/
public void setMultiplier(double multiplier) {
checkMultiplier(multiplier);
this.multiplier = multiplier;
}

/**
* Return the value to multiply the current interval by for each retry attempt.
*/
public double getMultiplier() {
return this.multiplier;
}

/**
* The maximum back off time.
*/
public void setMaxInterval(long maxInterval) {
this.maxInterval = maxInterval;
}

/**
* Return the maximum back off time.
*/
public long getMaxInterval() {
return this.maxInterval;
}

/**
* The maximum elapsed time in milliseconds after which a call to
* {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}.
*/
public void setMaxElapsedTime(long maxElapsedTime) {
this.maxElapsedTime = maxElapsedTime;
}

/**
* Return the maximum elapsed time in milliseconds after which a call to
* {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}.
*/
public long getMaxElapsedTime() {
return this.maxElapsedTime;
}

@Override
public BackOffExecution start() {
return new ExponentialBackOffExecution();
}

private void checkMultiplier(double multiplier) {
checkArgument(multiplier >= 1, "Invalid multiplier '" + multiplier + "'. Should be greater than " +
"or equal to 1. A multiplier of 1 is equivalent to a fixed interval.");
}

private class ExponentialBackOffExecution implements BackOffExecution {

private long currentInterval = -1;

private long currentElapsedTime = 0;

@Override
public long nextBackOff() {
if (this.currentElapsedTime >= maxElapsedTime) {
return STOP;
}

long nextInterval = computeNextInterval();
this.currentElapsedTime += nextInterval;
return nextInterval;
}

private long computeNextInterval() {
long maxInterval = getMaxInterval();
if (this.currentInterval >= maxInterval) {
return maxInterval;
} else if (this.currentInterval < 0) {
long initialInterval = getInitialInterval();
this.currentInterval = (initialInterval < maxInterval
? initialInterval : maxInterval);
} else {
this.currentInterval = multiplyInterval(maxInterval);
}
return this.currentInterval;
}

private long multiplyInterval(long maxInterval) {
long i = this.currentInterval;
i *= getMultiplier();
return (i > maxInterval ? maxInterval : i);
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder("ExponentialBackOff{");
sb.append("currentInterval=").append(this.currentInterval < 0 ? "n/a" : this.currentInterval + "ms");
sb.append(", multiplier=").append(getMultiplier());
sb.append('}');
return sb.toString();
}
}

}
Loading