HADOOP-14851 LambdaTestUtils.eventually() doesn't spin on Assertion failures. Contributed by Steve Loughran

This commit is contained in:
Aaron Fabbri 2017-09-08 19:26:27 -07:00
parent 3fddabc2fe
commit 180e814b08
No known key found for this signature in database
GPG Key ID: B2EEFA9E78118A29
2 changed files with 163 additions and 32 deletions

View File

@ -70,7 +70,7 @@ public final class LambdaTestUtils {
* @throws Exception if the handler wishes to raise an exception * @throws Exception if the handler wishes to raise an exception
* that way. * that way.
*/ */
Exception evaluate(int timeoutMillis, Exception caught) throws Exception; Throwable evaluate(int timeoutMillis, Throwable caught) throws Throwable;
} }
/** /**
@ -116,7 +116,7 @@ public final class LambdaTestUtils {
Preconditions.checkNotNull(timeoutHandler); Preconditions.checkNotNull(timeoutHandler);
long endTime = Time.now() + timeoutMillis; long endTime = Time.now() + timeoutMillis;
Exception ex = null; Throwable ex = null;
boolean running = true; boolean running = true;
int iterations = 0; int iterations = 0;
while (running) { while (running) {
@ -128,9 +128,11 @@ public final class LambdaTestUtils {
// the probe failed but did not raise an exception. Reset any // the probe failed but did not raise an exception. Reset any
// exception raised by a previous probe failure. // exception raised by a previous probe failure.
ex = null; ex = null;
} catch (InterruptedException | FailFastException e) { } catch (InterruptedException
| FailFastException
| VirtualMachineError e) {
throw e; throw e;
} catch (Exception e) { } catch (Throwable e) {
LOG.debug("eventually() iteration {}", iterations, e); LOG.debug("eventually() iteration {}", iterations, e);
ex = e; ex = e;
} }
@ -145,7 +147,9 @@ public final class LambdaTestUtils {
} }
} }
// timeout // timeout
Exception evaluate = timeoutHandler.evaluate(timeoutMillis, ex); Throwable evaluate;
try {
evaluate = timeoutHandler.evaluate(timeoutMillis, ex);
if (evaluate == null) { if (evaluate == null) {
// bad timeout handler logic; fall back to GenerateTimeout so the // bad timeout handler logic; fall back to GenerateTimeout so the
// underlying problem isn't lost. // underlying problem isn't lost.
@ -153,7 +157,10 @@ public final class LambdaTestUtils {
timeoutHandler); timeoutHandler);
evaluate = new GenerateTimeout().evaluate(timeoutMillis, ex); evaluate = new GenerateTimeout().evaluate(timeoutMillis, ex);
} }
throw evaluate; } catch (Throwable throwable) {
evaluate = throwable;
}
return raise(evaluate);
} }
/** /**
@ -217,6 +224,7 @@ public final class LambdaTestUtils {
* @throws Exception the last exception thrown before timeout was triggered * @throws Exception the last exception thrown before timeout was triggered
* @throws FailFastException if raised -without any retry attempt. * @throws FailFastException if raised -without any retry attempt.
* @throws InterruptedException if interrupted during the sleep operation. * @throws InterruptedException if interrupted during the sleep operation.
* @throws OutOfMemoryError you've run out of memory.
*/ */
public static <T> T eventually(int timeoutMillis, public static <T> T eventually(int timeoutMillis,
Callable<T> eval, Callable<T> eval,
@ -224,7 +232,7 @@ public final class LambdaTestUtils {
Preconditions.checkArgument(timeoutMillis >= 0, Preconditions.checkArgument(timeoutMillis >= 0,
"timeoutMillis must be >= 0"); "timeoutMillis must be >= 0");
long endTime = Time.now() + timeoutMillis; long endTime = Time.now() + timeoutMillis;
Exception ex; Throwable ex;
boolean running; boolean running;
int sleeptime; int sleeptime;
int iterations = 0; int iterations = 0;
@ -232,10 +240,12 @@ public final class LambdaTestUtils {
iterations++; iterations++;
try { try {
return eval.call(); return eval.call();
} catch (InterruptedException | FailFastException e) { } catch (InterruptedException
| FailFastException
| VirtualMachineError e) {
// these two exceptions trigger an immediate exit // these two exceptions trigger an immediate exit
throw e; throw e;
} catch (Exception e) { } catch (Throwable e) {
LOG.debug("evaluate() iteration {}", iterations, e); LOG.debug("evaluate() iteration {}", iterations, e);
ex = e; ex = e;
} }
@ -245,7 +255,26 @@ public final class LambdaTestUtils {
} }
} while (running); } while (running);
// timeout. Throw the last exception raised // timeout. Throw the last exception raised
throw ex; return raise(ex);
}
/**
* Take the throwable and raise it as an exception or an error, depending
* upon its type. This allows callers to declare that they only throw
* Exception (i.e. can be invoked by Callable) yet still rethrow a
* previously caught Throwable.
* @param throwable Throwable to rethrow
* @param <T> expected return type
* @return never
* @throws Exception if throwable is an Exception
* @throws Error if throwable is not an Exception
*/
private static <T> T raise(Throwable throwable) throws Exception {
if (throwable instanceof Exception) {
throw (Exception) throwable;
} else {
throw (Error) throwable;
}
} }
/** /**
@ -365,6 +394,7 @@ public final class LambdaTestUtils {
* @throws Exception any other exception raised * @throws Exception any other exception raised
* @throws AssertionError if the evaluation call didn't raise an exception. * @throws AssertionError if the evaluation call didn't raise an exception.
*/ */
@SuppressWarnings("unchecked")
public static <E extends Throwable> E intercept( public static <E extends Throwable> E intercept(
Class<E> clazz, Class<E> clazz,
VoidCallable eval) VoidCallable eval)
@ -487,14 +517,14 @@ public final class LambdaTestUtils {
* @return TimeoutException * @return TimeoutException
*/ */
@Override @Override
public Exception evaluate(int timeoutMillis, Exception caught) public Throwable evaluate(int timeoutMillis, Throwable caught)
throws Exception { throws Throwable {
String s = String.format("%s: after %d millis", message, String s = String.format("%s: after %d millis", message,
timeoutMillis); timeoutMillis);
String caughtText = caught != null String caughtText = caught != null
? ("; " + robustToString(caught)) : ""; ? ("; " + robustToString(caught)) : "";
return (TimeoutException) (new TimeoutException(s + caughtText) return (new TimeoutException(s + caughtText)
.initCause(caught)); .initCause(caught));
} }
} }

View File

@ -25,6 +25,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import static org.apache.hadoop.test.LambdaTestUtils.*; import static org.apache.hadoop.test.LambdaTestUtils.*;
import static org.apache.hadoop.test.GenericTestUtils.*; import static org.apache.hadoop.test.GenericTestUtils.*;
@ -123,6 +124,27 @@ public class TestLambdaTestUtils extends Assert {
minCount <= retry.getInvocationCount()); minCount <= retry.getInvocationCount());
} }
/**
* Raise an exception.
* @param e exception to raise
* @return never
* @throws Exception passed in exception
*/
private boolean r(Exception e) throws Exception {
throw e;
}
/**
* Raise an error.
* @param e error to raise
* @return never
* @throws Exception never
* @throws Error the passed in error
*/
private boolean r(Error e) throws Exception {
throw e;
}
@Test @Test
public void testAwaitAlwaysTrue() throws Throwable { public void testAwaitAlwaysTrue() throws Throwable {
await(TIMEOUT, await(TIMEOUT,
@ -140,7 +162,7 @@ public class TestLambdaTestUtils extends Assert {
TIMEOUT_FAILURE_HANDLER); TIMEOUT_FAILURE_HANDLER);
fail("should not have got here"); fail("should not have got here");
} catch (TimeoutException e) { } catch (TimeoutException e) {
assertTrue(retry.getInvocationCount() > 4); assertMinRetryCount(1);
} }
} }
@ -316,9 +338,7 @@ public class TestLambdaTestUtils extends Assert {
IOException ioe = intercept(IOException.class, IOException ioe = intercept(IOException.class,
() -> await( () -> await(
TIMEOUT, TIMEOUT,
() -> { () -> r(new IOException("inner " + ++count)),
throw new IOException("inner " + ++count);
},
retry, retry,
(timeout, ex) -> ex)); (timeout, ex) -> ex));
assertRetryCount(count - 1); assertRetryCount(count - 1);
@ -339,9 +359,7 @@ public class TestLambdaTestUtils extends Assert {
public void testInterceptAwaitFailFastLambda() throws Throwable { public void testInterceptAwaitFailFastLambda() throws Throwable {
intercept(FailFastException.class, intercept(FailFastException.class,
() -> await(TIMEOUT, () -> await(TIMEOUT,
() -> { () -> r(new FailFastException("ffe")),
throw new FailFastException("ffe");
},
retry, retry,
(timeout, ex) -> ex)); (timeout, ex) -> ex));
assertRetryCount(0); assertRetryCount(0);
@ -361,14 +379,13 @@ public class TestLambdaTestUtils extends Assert {
assertRetryCount(0); assertRetryCount(0);
} }
@Test @Test
public void testInterceptEventuallyLambdaFailures() throws Throwable { public void testInterceptEventuallyLambdaFailures() throws Throwable {
intercept(IOException.class, intercept(IOException.class,
"oops", "oops",
() -> eventually(TIMEOUT, () -> eventually(TIMEOUT,
() -> { () -> r(new IOException("oops")),
throw new IOException("oops");
},
retry)); retry));
assertMinRetryCount(1); assertMinRetryCount(1);
} }
@ -385,11 +402,95 @@ public class TestLambdaTestUtils extends Assert {
intercept(FailFastException.class, "oops", intercept(FailFastException.class, "oops",
() -> eventually( () -> eventually(
TIMEOUT, TIMEOUT,
() -> { () -> r(new FailFastException("oops")),
throw new FailFastException("oops");
},
retry)); retry));
assertRetryCount(0); assertRetryCount(0);
} }
/**
* Verify that assertions trigger catch and retry.
* @throws Throwable if the code is broken
*/
@Test
public void testEventuallySpinsOnAssertions() throws Throwable {
AtomicInteger counter = new AtomicInteger(0);
eventually(TIMEOUT,
() -> {
while (counter.incrementAndGet() < 5) {
fail("if you see this, we are in trouble");
}
},
retry);
assertMinRetryCount(4);
}
/**
* Verify that VirtualMachineError errors are immediately rethrown.
* @throws Throwable if the code is broken
*/
@Test
public void testInterceptEventuallyThrowsVMErrors() throws Throwable {
intercept(OutOfMemoryError.class, "OOM",
() -> eventually(
TIMEOUT,
() -> r(new OutOfMemoryError("OOM")),
retry));
assertRetryCount(0);
}
/**
* Verify that you can declare that an intercept will intercept Errors.
* @throws Throwable if the code is broken
*/
@Test
public void testInterceptHandlesErrors() throws Throwable {
intercept(OutOfMemoryError.class, "OOM",
() -> r(new OutOfMemoryError("OOM")));
}
/**
* Verify that if an Error raised is not the one being intercepted,
* it gets rethrown.
* @throws Throwable if the code is broken
*/
@Test
public void testInterceptRethrowsVMErrors() throws Throwable {
intercept(StackOverflowError.class, "",
() -> intercept(OutOfMemoryError.class, "",
() -> r(new StackOverflowError())));
}
@Test
public void testAwaitHandlesAssertions() throws Throwable {
// await a state which is never reached, expect a timeout exception
// with the text "failure" in it
TimeoutException ex = intercept(TimeoutException.class,
"failure",
() -> await(TIMEOUT,
() -> r(new AssertionError("failure")),
retry,
TIMEOUT_FAILURE_HANDLER));
// the retry handler must have been invoked
assertMinRetryCount(1);
// and the nested cause is tha raised assertion
if (!(ex.getCause() instanceof AssertionError)) {
throw ex;
}
}
@Test
public void testAwaitRethrowsVMErrors() throws Throwable {
// await a state which is never reached, expect a timeout exception
// with the text "failure" in it
intercept(StackOverflowError.class,
() -> await(TIMEOUT,
() -> r(new StackOverflowError()),
retry,
TIMEOUT_FAILURE_HANDLER));
// the retry handler must not have been invoked
assertMinRetryCount(0);
}
} }