diff --git a/core/src/main/java/org/jclouds/predicates/PredicateCallable.java b/core/src/main/java/org/jclouds/predicates/PredicateCallable.java new file mode 100644 index 0000000000..37b7053fc4 --- /dev/null +++ b/core/src/main/java/org/jclouds/predicates/PredicateCallable.java @@ -0,0 +1,43 @@ +package org.jclouds.predicates; + +import java.util.concurrent.Callable; + +public abstract class PredicateCallable implements PredicateWithResult, Callable { + + Result lastResult; + Exception lastFailure; + + @Override + public boolean apply(Void input) { + try { + lastResult = call(); + onCompletion(); + return isAcceptable(lastResult); + } catch (Exception e) { + lastFailure = e; + onFailure(); + return false; + } + } + + protected void onFailure() { + } + + protected void onCompletion() { + } + + protected boolean isAcceptable(Result result) { + return result!=null; + } + + @Override + public Result getResult() { + return lastResult; + } + + @Override + public Throwable getLastFailure() { + return lastFailure; + } + +} diff --git a/core/src/main/java/org/jclouds/predicates/PredicateWithResult.java b/core/src/main/java/org/jclouds/predicates/PredicateWithResult.java new file mode 100644 index 0000000000..dae1e5c98a --- /dev/null +++ b/core/src/main/java/org/jclouds/predicates/PredicateWithResult.java @@ -0,0 +1,11 @@ +package org.jclouds.predicates; + +import com.google.common.base.Predicate; + +public interface PredicateWithResult extends Predicate { + + Result getResult(); + + Throwable getLastFailure(); + +} diff --git a/core/src/main/java/org/jclouds/predicates/RetryablePredicate.java b/core/src/main/java/org/jclouds/predicates/RetryablePredicate.java index cf203cf601..9b093bc747 100644 --- a/core/src/main/java/org/jclouds/predicates/RetryablePredicate.java +++ b/core/src/main/java/org/jclouds/predicates/RetryablePredicate.java @@ -34,6 +34,11 @@ import com.google.common.base.Predicate; /** * * Retries a condition until it is met or a timeout occurs. + * maxWait parameter is required. + * Initial retry period and retry maxPeriod are optionally configurable, + * defaulting to 50ms and 1000ms respectively, + * with the retrier increasing the interval by a factor of 1.5 each time within these constraints. + * All values taken as millis unless TimeUnit specified. * * @author Adrian Cole */ @@ -92,6 +97,7 @@ public class RetryablePredicate implements Predicate { } protected long nextMaxInterval(long attempt, Date end) { + // FIXME i think this should be pow(1.5, attempt) -- or alternatively newInterval = oldInterval*1.5 long interval = (period * (long) Math.pow(attempt, 1.5)); interval = interval > maxPeriod ? maxPeriod : interval; long max = end.getTime() - System.currentTimeMillis(); diff --git a/core/src/main/java/org/jclouds/predicates/Retryables.java b/core/src/main/java/org/jclouds/predicates/Retryables.java new file mode 100644 index 0000000000..2de06a3725 --- /dev/null +++ b/core/src/main/java/org/jclouds/predicates/Retryables.java @@ -0,0 +1,36 @@ +package org.jclouds.predicates; + +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Predicate; + +public class Retryables { + + public static boolean retry(Predicate predicate, Input input, long maxWaitMillis) { + return new RetryablePredicate(predicate, maxWaitMillis).apply(input); + } + + public static boolean retry(Predicate predicate, Input input, long maxWait, long period, TimeUnit unit) { + return new RetryablePredicate(predicate, maxWait, period, unit).apply(input); + } + + public static void assertEventually(Predicate predicate, Input input, + long maxWaitMillis, String failureMessage) { + if (!new RetryablePredicate(predicate, maxWaitMillis).apply(input)) + throw new AssertionError(failureMessage); + } + + public static Result retryGettingResultOrFailing(PredicateWithResult predicate, + Input input, long maxWaitMillis, String failureMessage) { + if (!new RetryablePredicate(predicate, maxWaitMillis).apply(input)) + throw (AssertionError)new AssertionError(failureMessage).initCause(predicate.getLastFailure()); + return predicate.getResult(); + } + public static Result retryGettingResultOrFailing(PredicateWithResult predicate, + Input input, long maxWait, long period, TimeUnit unit, String failureMessage) { + if (!new RetryablePredicate(predicate, maxWait, period, unit).apply(input)) + throw (AssertionError)new AssertionError(failureMessage).initCause(predicate.getLastFailure()); + return predicate.getResult(); + } + +} diff --git a/core/src/test/java/org/jclouds/predicates/RetryablesTest.java b/core/src/test/java/org/jclouds/predicates/RetryablesTest.java new file mode 100644 index 0000000000..c49931d39a --- /dev/null +++ b/core/src/test/java/org/jclouds/predicates/RetryablesTest.java @@ -0,0 +1,110 @@ +package org.jclouds.predicates; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.jclouds.predicates.Retryables.retry; +import static org.jclouds.predicates.Retryables.retryGettingResultOrFailing; +import static org.testng.Assert.*; + +import org.testng.annotations.Test; + +@Test +public class RetryablesTest { + + public static class FindX implements PredicateWithResult { + Character result; + Throwable lastFailure; + int attempts=0; + @Override + public boolean apply(String input) { + try { + result = input.charAt(attempts++); + return (result=='x'); + } catch (Exception e) { + lastFailure = e; + return false; + } + } + public Character getResult() { + return result; + } + public Throwable getLastFailure() { + return lastFailure; + } + } + + public void testPredicateWithResult() { + FindX findX = new FindX(); + assertFalse(findX.apply("hexy")); + assertEquals((char)findX.getResult(), 'h'); + assertFalse(findX.apply("hexy")); + assertTrue(findX.apply("hexy")); + assertEquals((char)findX.getResult(), 'x'); + + assertFalse(findX.apply("hexy")); + assertNull(findX.getLastFailure()); + //now we get error + assertFalse(findX.apply("hexy")); + assertNotNull(findX.getLastFailure()); + assertEquals((char)findX.getResult(), 'y'); + } + + public void testRetry() { + FindX findX = new FindX(); + assertTrue(retry(findX, "hexy", 1000, 1, MILLISECONDS)); + assertEquals(findX.attempts, 3); + assertEquals((char)findX.getResult(), 'x'); + assertNull(findX.getLastFailure()); + + //now we'll be getting errors + assertFalse(retry(findX, "hexy", 100, 1, MILLISECONDS)); + assertEquals((char)findX.getResult(), 'y'); + assertNotNull(findX.getLastFailure()); + } + + public void testRetryGetting() { + FindX findX = new FindX(); + assertEquals((char)retryGettingResultOrFailing(findX, "hexy", 1000, "shouldn't happen"), 'x'); + + //now we'll be getting errors + boolean secondRetrySucceeds=false; + try { + retryGettingResultOrFailing(findX, "hexy", 100, "expected"); + secondRetrySucceeds = true; + } catch (AssertionError e) { + assertTrue(e.toString().contains("expected")); + } + if (secondRetrySucceeds) fail("should have thrown"); + assertNotNull(findX.getLastFailure()); + assertFalse(findX.getLastFailure().toString().contains("expected")); + } + + //using PredicateCallable we can repeat the above test, with the job expressed more simply + public static class FindXSimpler extends PredicateCallable { + String input = "hexy"; + int attempts=0; + public Character call() { + return input.charAt(attempts++); + } + public boolean isAcceptable(Character result) { + return result=='x'; + } + } + + public void testSimplerPredicateCallableRetryGetting() { + FindXSimpler findX = new FindXSimpler(); + assertEquals((char)retryGettingResultOrFailing(findX, null, 1000, "shouldn't happen"), 'x'); + + //now we'll be getting errors + boolean secondRetrySucceeds=false; + try { + retryGettingResultOrFailing(findX, null, 100, "expected"); + secondRetrySucceeds = true; + } catch (AssertionError e) { + assertTrue(e.toString().contains("expected")); + } + if (secondRetrySucceeds) fail("should have thrown"); + assertNotNull(findX.getLastFailure()); + assertFalse(findX.getLastFailure().toString().contains("expected")); + } + +}