mirror of https://github.com/apache/jclouds.git
Fix/improve retry-predicate:
- If get timeout of 0 (or negative), then still try once. - Remove (unlikely) race in retry’s apply(T) where context-switching delays could cause `before(end)` to return false the first time, even though the timeout was positive. - Ensure retries at end of the timeout (e.g. if timeout is 30 secs and last sleep takes us up to the 30 secs mark, then test again rather than returning immediately after the sleep!) - Use `long` for time, rather than `java.util.Date`, for internal calculations. Deprecates old protected methods that use Date.
This commit is contained in:
parent
db9fec0bbb
commit
7866612a2e
|
@ -104,19 +104,30 @@ public class Predicates2 {
|
|||
|
||||
@Override
|
||||
public boolean apply(T input) {
|
||||
// Ensure that try one last time at the end (e.g. if timeout is 30 secs and last sleep
|
||||
// takes us up to the 30 secs mark, then test again rather than returning immediately
|
||||
// after the sleep!).
|
||||
// Always try at least once, even if timeout is 0 or negative.
|
||||
// A timeout of 0 means 0 millis.
|
||||
try {
|
||||
long i = 1l;
|
||||
for (Date end = new Date(System.currentTimeMillis() + timeout); before(end); Thread.sleep(nextMaxInterval(i++,
|
||||
end))) {
|
||||
long now = System.currentTimeMillis();
|
||||
long end = now + timeout;
|
||||
while (now < end) {
|
||||
if (findOrBreak.apply(input)) {
|
||||
return true;
|
||||
} else if (atOrAfter(end)) {
|
||||
return false;
|
||||
}
|
||||
long sleepTime = nextMaxInterval(i++, end);
|
||||
if (sleepTime > 0) Thread.sleep(sleepTime);
|
||||
now = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
return findOrBreak.apply(input);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn(e, "predicate %s on %s interrupted, returning false", input, findOrBreak);
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
} catch (RuntimeException e) {
|
||||
if (getFirstThrowableOfType(e, ExecutionException.class) != null) {
|
||||
logger.warn(e, "predicate %s on %s errored [%s], returning false", input, findOrBreak, e.getMessage());
|
||||
|
@ -133,7 +144,13 @@ public class Predicates2 {
|
|||
} else
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.9.0; use {@link #nextMaxInterval(long, long)}
|
||||
*/
|
||||
protected long nextMaxInterval(long attempt, Date end) {
|
||||
return nextMaxInterval(attempt, end.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -142,21 +159,27 @@ public class Predicates2 {
|
|||
* (where 1.5 is the backoff factor), to the maximum interval or specified timeout.
|
||||
*
|
||||
* @param attempt number of this attempt (starting at 1 for the first retry)
|
||||
* @param end timeout
|
||||
* @param endTime timeout
|
||||
* @return time in milliseconds from now until the next attempt, or if negative, time lapsed
|
||||
* since the specified timeout
|
||||
*/
|
||||
protected long nextMaxInterval(long attempt, Date end) {
|
||||
protected long nextMaxInterval(long attempt, long endTime) {
|
||||
long interval = (long) (period * Math.pow(1.5, attempt - 1));
|
||||
interval = interval > maxPeriod ? maxPeriod : interval;
|
||||
long max = end.getTime() - System.currentTimeMillis();
|
||||
long max = endTime - System.currentTimeMillis();
|
||||
return (interval > max) ? max : interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.9.0; use {@link #nextMaxInterval(long, long)}
|
||||
*/
|
||||
protected boolean before(Date end) {
|
||||
return new Date().compareTo(end) <= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.9.0; use {@link #nextMaxInterval(long, long)}
|
||||
*/
|
||||
protected boolean atOrAfter(Date end) {
|
||||
return new Date().compareTo(end) >= 0;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ package org.jclouds.util;
|
|||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.jclouds.util.Predicates2.retry;
|
||||
import static org.testng.Assert.assertFalse;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
import static org.testng.Assert.fail;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
@ -31,7 +33,6 @@ import org.testng.annotations.BeforeMethod;
|
|||
import org.testng.annotations.Test;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.Lists;
|
||||
|
@ -52,29 +53,23 @@ public class Predicates2Test {
|
|||
}
|
||||
|
||||
@Test
|
||||
void testFalseOnIllegalStateExeception() {
|
||||
void testRetryReturnsFalseOnIllegalStateExeception() {
|
||||
ensureImmediateReturnFor(new IllegalStateException());
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Test
|
||||
void testFalseOnExecutionException() {
|
||||
ensureImmediateReturnFor(new ExecutionException() {
|
||||
});
|
||||
void testRetryReturnsFalseOnExecutionException() {
|
||||
ensureImmediateReturnFor(new ExecutionException(new Exception("Simulated cause")));
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Test
|
||||
void testFalseOnTimeoutException() {
|
||||
ensureImmediateReturnFor(new TimeoutException() {
|
||||
});
|
||||
void testRetryReturnsFalseOnTimeoutException() {
|
||||
ensureImmediateReturnFor(new TimeoutException("Simulating exception"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Test(expectedExceptions = RuntimeException.class)
|
||||
void testPropagateOnException() {
|
||||
ensureImmediateReturnFor(new Exception() {
|
||||
});
|
||||
void testRetryPropagatesOnException() {
|
||||
ensureImmediateReturnFor(new Exception("Simulating exception"));
|
||||
}
|
||||
|
||||
private void ensureImmediateReturnFor(final Exception ex) {
|
||||
|
@ -86,48 +81,66 @@ public class Predicates2Test {
|
|||
}, 3, 1, SECONDS);
|
||||
|
||||
stopwatch.start();
|
||||
assert !predicate.apply(new Supplier<String>() {
|
||||
assertFalse(predicate.apply(new Supplier<String>() {
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
});
|
||||
}));
|
||||
long duration = stopwatch.elapsed(MILLISECONDS);
|
||||
assertOrdered(duration, SLOW_BUILD_SERVER_GRACE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAlwaysTrue() {
|
||||
// will call once immediately
|
||||
Predicate<String> predicate = retry(Predicates.<String> alwaysTrue(), 3, 1, SECONDS);
|
||||
stopwatch.start();
|
||||
predicate.apply("");
|
||||
long duration = stopwatch.elapsed(MILLISECONDS);
|
||||
assertOrdered(duration, SLOW_BUILD_SERVER_GRACE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAlwaysFalseMillis() {
|
||||
void testRetryAlwaysFalseMillis() {
|
||||
// maxWait=3; period=1; maxPeriod defaults to 1*10
|
||||
// will call at 0, 1, 1+(1*1.5), 3
|
||||
Predicate<String> predicate = retry(Predicates.<String> alwaysFalse(), 3, 1, SECONDS);
|
||||
RepeatedAttemptsPredicate rawPredicate = new RepeatedAttemptsPredicate(Integer.MAX_VALUE);
|
||||
Predicate<String> predicate = retry(rawPredicate, 3, 1, SECONDS);
|
||||
stopwatch.start();
|
||||
predicate.apply("");
|
||||
assertFalse(predicate.apply(""));
|
||||
long duration = stopwatch.elapsed(MILLISECONDS);
|
||||
assertOrdered(3000 - EARLY_RETURN_GRACE, duration, 3000 + SLOW_BUILD_SERVER_GRACE);
|
||||
assertCallTimes(rawPredicate.callTimes, 0, 1000, 1000 + 1500, 3000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testThirdTimeTrue() {
|
||||
// maxWait=4; period=1; maxPeriod defaults to 1*10
|
||||
// will call at 0, 1, 1+(1*1.5)
|
||||
RepeatedAttemptsPredicate rawPredicate = new RepeatedAttemptsPredicate(2);
|
||||
void testRetryFirstTimeTrue() {
|
||||
RepeatedAttemptsPredicate rawPredicate = new RepeatedAttemptsPredicate(1);
|
||||
Predicate<String> predicate = retry(rawPredicate, 4, 1, SECONDS);
|
||||
|
||||
stopwatch.start();
|
||||
predicate.apply("");
|
||||
assertTrue(predicate.apply(""));
|
||||
long duration = stopwatch.elapsed(MILLISECONDS);
|
||||
|
||||
assertOrdered(0, duration, 0 + SLOW_BUILD_SERVER_GRACE);
|
||||
assertCallTimes(rawPredicate.callTimes, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetryWillRunOnceOnNegativeTimeout() {
|
||||
RepeatedAttemptsPredicate rawPredicate = new RepeatedAttemptsPredicate(1);
|
||||
Predicate<String> predicate = retry(rawPredicate, -1, 1, SECONDS);
|
||||
|
||||
stopwatch.start();
|
||||
assertTrue(predicate.apply(""));
|
||||
long duration = stopwatch.elapsed(MILLISECONDS);
|
||||
|
||||
assertOrdered(0, duration, 0 + SLOW_BUILD_SERVER_GRACE);
|
||||
assertCallTimes(rawPredicate.callTimes, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetryThirdTimeTrue() {
|
||||
// maxWait=4; period=1; maxPeriod defaults to 1*10
|
||||
// will call at 0, 1, 1+(1*1.5)
|
||||
RepeatedAttemptsPredicate rawPredicate = new RepeatedAttemptsPredicate(3);
|
||||
Predicate<String> predicate = retry(rawPredicate, 4, 1, SECONDS);
|
||||
|
||||
stopwatch.start();
|
||||
assertTrue(predicate.apply(""));
|
||||
long duration = stopwatch.elapsed(MILLISECONDS);
|
||||
|
||||
assertOrdered(2500 - EARLY_RETURN_GRACE, duration, 2500 + SLOW_BUILD_SERVER_GRACE);
|
||||
|
@ -135,14 +148,14 @@ public class Predicates2Test {
|
|||
}
|
||||
|
||||
@Test
|
||||
void testThirdTimeTrueLimitedMaxInterval() {
|
||||
void testRetryThirdTimeTrueLimitedMaxInterval() {
|
||||
// maxWait=3; period=1; maxPeriod=1
|
||||
// will call at 0, 1, 1+1
|
||||
RepeatedAttemptsPredicate rawPredicate = new RepeatedAttemptsPredicate(2);
|
||||
RepeatedAttemptsPredicate rawPredicate = new RepeatedAttemptsPredicate(3);
|
||||
Predicate<String> predicate = retry(rawPredicate, 3, 1, 1, SECONDS);
|
||||
|
||||
stopwatch.start();
|
||||
predicate.apply("");
|
||||
assertTrue(predicate.apply(""));
|
||||
long duration = stopwatch.elapsed(MILLISECONDS);
|
||||
|
||||
assertOrdered(2000 - EARLY_RETURN_GRACE, duration, 2000 + SLOW_BUILD_SERVER_GRACE);
|
||||
|
@ -163,13 +176,13 @@ public class Predicates2Test {
|
|||
@Override
|
||||
public boolean apply(String input) {
|
||||
callTimes.add(stopwatch.elapsed(MILLISECONDS));
|
||||
return count++ == succeedOnAttempt;
|
||||
return ++count == succeedOnAttempt;
|
||||
}
|
||||
}
|
||||
|
||||
@Test(enabled = false) // not a test, but picked up as such because public
|
||||
public static void assertCallTimes(List<Long> actual, Integer... expected) {
|
||||
Assert.assertEquals(actual.size(), expected.length);
|
||||
Assert.assertEquals(actual.size(), expected.length, "actual=" + actual);
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
long callTime = actual.get(i);
|
||||
assertOrdered(expected[i] - EARLY_RETURN_GRACE, callTime, expected[i] + SLOW_BUILD_SERVER_GRACE);
|
||||
|
|
Loading…
Reference in New Issue