Add BackOffExecution to isolate state

This commit separates the BackOff configuration from an actual
 execution. BackOffExecution now contains all the state of a
 particular execution and BackOff is only meant to start (i.e.
 create) a new execution.

 The method "reset" has been removed as its no longer necessary:
 when an execution does not need to be used for a given operation
 anymore it can be simply discarded.

 Issue: SPR-11746
This commit is contained in:
Stephane Nicoll
2014-05-09 16:39:01 +02:00
parent 49040a2925
commit 89fc3c0257
10 changed files with 270 additions and 167 deletions

View File

@@ -17,16 +17,19 @@
package org.springframework.util;
/**
* Indicate the rate at which an operation should be retried.
* 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">
* {@code
*
* long waitInterval = backOff.nextBackOffMillis();
* if (waitInterval == BackOff.STOP) {
* backOff.reset();
* BackOffExecution exec = backOff.start();
*
* // In the operation recovery/retry loop:
* long waitInterval = exec.nextBackOffMillis();
* if (waitInterval == BackOffExecution.STOP) {
* // do not retry operation
* }
* else {
@@ -35,31 +38,19 @@ package org.springframework.util;
* }
* }</pre>
*
* Once the underlying operation has completed successfully, the instance
* <b>must</b> be {@link #reset()} before further use. Due to how back off
* should be used, implementations do not need to be thread-safe.
* Once the underlying operation has completed successfully, the execution
* instance can be simply discarded.
*
* @author Stephane Nicoll
* @since 4.1
* @see BackOffExecution
*/
public interface BackOff {
/**
* Return value of {@link #nextBackOff()} that indicates that the operation
* should not be retried.
* Start a new back off execution.
* @return a fresh {@link BackOffExecution} ready to be used
*/
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();
/**
* Reset this instance to its original state.
*/
void reset();
BackOffExecution start();
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* 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 org.springframework.util;
/**
* Represent a particular back-off execution.
*
* <p>Implementations do not need to be thread safe.
*
* @author Stephane Nicoll
* @since 4.1
* @see org.springframework.util.BackOff
*/
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();
}

View File

@@ -44,7 +44,8 @@ package org.springframework.util;
*
* Note that the default max elapsed time is {@link Long#MAX_VALUE}. Use
* {@link #setMaxElapsedTime(long)} to limit the maximum number of time
* that an instance should accumulate before returning {@link BackOff#STOP}.
* that an instance should accumulate before returning
* {@link BackOffExecution#STOP}.
*
* @author Stephane Nicoll
* @since 4.1
@@ -80,9 +81,6 @@ public class ExponentialBackOff implements BackOff {
private long maxElapsedTime = DEFAULT_MAX_ELAPSED_TIME;
private long currentInterval = -1;
private long currentElapsedTime = 0;
/**
* Create an instance with the default settings.
@@ -112,6 +110,13 @@ public class ExponentialBackOff implements BackOff {
this.initialInterval = initialInterval;
}
/**
* Return the initial interval in milliseconds.
*/
public long getInitialInterval() {
return initialInterval;
}
/**
* The value to multiply the current interval with for each retry attempt.
*/
@@ -120,6 +125,13 @@ public class ExponentialBackOff implements BackOff {
this.multiplier = multiplier;
}
/**
* Return the value to multiply the current interval with for each retry attempt.
*/
public double getMultiplier() {
return multiplier;
}
/**
* The maximum back off time.
*/
@@ -127,50 +139,32 @@ public class ExponentialBackOff implements BackOff {
this.maxInterval = maxInterval;
}
/**
* Return the maximum back off time.
*/
public long getMaxInterval() {
return maxInterval;
}
/**
* The maximum elapsed time in milliseconds after which a call to
* {@link #nextBackOff()} returns {@link BackOff#STOP}.
* {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}.
*/
public void setMaxElapsedTime(long maxElapsedTime) {
this.maxElapsedTime = maxElapsedTime;
}
@Override
public long nextBackOff() {
if (currentElapsedTime >= maxElapsedTime) {
return BackOff.STOP;
}
long nextInterval = computeNextInterval();
currentElapsedTime += nextInterval;
return nextInterval;
/**
* Return the maximum elapsed time in milliseconds after which a call to
* {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}.
*/
public long getMaxElapsedTime() {
return maxElapsedTime;
}
@Override
public void reset() {
this.currentInterval = -1;
this.currentElapsedTime = 0;
}
private long computeNextInterval() {
if (this.currentInterval >= this.maxInterval) {
return this.maxInterval;
}
else if (this.currentInterval < 0) {
this.currentInterval = (this.initialInterval < this.maxInterval
? this.initialInterval : this.maxInterval);
}
else {
this.currentInterval = multiplyInterval();
}
return currentInterval;
}
private long multiplyInterval() {
long i = this.currentInterval;
i *= this.multiplier;
return (i > this.maxInterval ? this.maxInterval :i);
public BackOffExecution start() {
return new ExponentialBackOffExecution();
}
private void checkMultiplier(double multiplier) {
@@ -180,14 +174,56 @@ public class ExponentialBackOff implements BackOff {
}
}
@Override
public String toString() {
String i = (this.currentInterval < 0 ? "n/a" : this.currentInterval + "ms");
final StringBuilder sb = new StringBuilder("ExponentialBackOff{");
sb.append("currentInterval=").append(i);
sb.append(", multiplier=").append(this.multiplier);
sb.append('}');
return sb.toString();
private class ExponentialBackOffExecution implements BackOffExecution {
private long currentInterval = -1;
private long currentElapsedTime = 0;
@Override
public long nextBackOff() {
if (currentElapsedTime >= maxElapsedTime) {
return BackOffExecution.STOP;
}
long nextInterval = computeNextInterval();
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 currentInterval;
}
private long multiplyInterval(long maxInterval) {
long i = this.currentInterval;
i *= getMultiplier();
return (i > maxInterval ? maxInterval : i);
}
@Override
public String toString() {
String i = (this.currentInterval < 0 ? "n/a" : this.currentInterval + "ms");
final StringBuilder sb = new StringBuilder("ExponentialBackOff{");
sb.append("currentInterval=").append(i);
sb.append(", multiplier=").append(getMultiplier());
sb.append('}');
return sb.toString();
}
}
}

View File

@@ -39,7 +39,6 @@ public class FixedBackOff implements BackOff {
private long maxAttempts = UNLIMITED_ATTEMPTS;
private long currentAttempts = 0;
/**
* Create an instance with an interval of {@value #DEFAULT_INTERVAL}
@@ -87,31 +86,38 @@ public class FixedBackOff implements BackOff {
}
@Override
public long nextBackOff() {
this.currentAttempts++;
if (this.currentAttempts <= this.maxAttempts) {
return this.interval;
}
else {
return BackOff.STOP;
}
public BackOffExecution start() {
return new FixedBackOffExecution();
}
@Override
public void reset() {
this.currentAttempts = 0;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("FixedBackOff{");
sb.append("interval=").append(this.interval);
String attemptValue = (this.maxAttempts == Long.MAX_VALUE ? "unlimited"
: String.valueOf(this.maxAttempts));
sb.append(", currentAttempts=").append(this.currentAttempts);
sb.append(", maxAttempts=").append(attemptValue);
sb.append('}');
return sb.toString();
private class FixedBackOffExecution implements BackOffExecution {
private long currentAttempts = 0;
@Override
public long nextBackOff() {
this.currentAttempts++;
if (this.currentAttempts <= getMaxAttempts()) {
return getInterval();
}
else {
return BackOffExecution.STOP;
}
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("FixedBackOff{");
sb.append("interval=").append(FixedBackOff.this.interval);
String attemptValue = (FixedBackOff.this.maxAttempts == Long.MAX_VALUE ? "unlimited"
: String.valueOf(FixedBackOff.this.maxAttempts));
sb.append(", currentAttempts=").append(this.currentAttempts);
sb.append(", maxAttempts=").append(attemptValue);
sb.append('}');
return sb.toString();
}
}
}