|
@@ -0,0 +1,521 @@
|
|
|
+/*
|
|
|
+ * Licensed to the Apache Software Foundation (ASF) under one
|
|
|
+ * or more contributor license agreements. See the NOTICE file
|
|
|
+ * distributed with this work for additional information
|
|
|
+ * regarding copyright ownership. The ASF licenses this file
|
|
|
+ * to you 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.apache.hadoop.test;
|
|
|
+
|
|
|
+import com.google.common.base.Preconditions;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import org.apache.hadoop.util.Time;
|
|
|
+
|
|
|
+import java.util.concurrent.Callable;
|
|
|
+import java.util.concurrent.TimeoutException;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Class containing methods and associated classes to make the most of Lambda
|
|
|
+ * expressions in Hadoop tests.
|
|
|
+ *
|
|
|
+ * The code has been designed from the outset to be Java-8 friendly, but
|
|
|
+ * to still be usable in Java 7.
|
|
|
+ *
|
|
|
+ * The code is modelled on {@code GenericTestUtils#waitFor(Supplier, int, int)},
|
|
|
+ * but also lifts concepts from Scalatest's {@code awaitResult} and
|
|
|
+ * its notion of pluggable retry logic (simple, backoff, maybe even things
|
|
|
+ * with jitter: test author gets to choose).
|
|
|
+ * The {@link #intercept(Class, Callable)} method is also all credit due
|
|
|
+ * Scalatest, though it's been extended to also support a string message
|
|
|
+ * check; useful when checking the contents of the exception.
|
|
|
+ */
|
|
|
+public final class LambdaTestUtils {
|
|
|
+ public static final Logger LOG =
|
|
|
+ LoggerFactory.getLogger(LambdaTestUtils.class);
|
|
|
+
|
|
|
+ private LambdaTestUtils() {
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This is the string included in the assertion text in
|
|
|
+ * {@link #intercept(Class, Callable)} if
|
|
|
+ * the closure returned a null value.
|
|
|
+ */
|
|
|
+ public static final String NULL_RESULT = "(null)";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Interface to implement for converting a timeout into some form
|
|
|
+ * of exception to raise.
|
|
|
+ */
|
|
|
+ public interface TimeoutHandler {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create an exception (or throw one, if desired).
|
|
|
+ * @param timeoutMillis timeout which has arisen
|
|
|
+ * @param caught any exception which was caught; may be null
|
|
|
+ * @return an exception which will then be thrown
|
|
|
+ * @throws Exception if the handler wishes to raise an exception
|
|
|
+ * that way.
|
|
|
+ */
|
|
|
+ Exception evaluate(int timeoutMillis, Exception caught) throws Exception;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Wait for a condition to be met, with a retry policy returning the
|
|
|
+ * sleep time before the next attempt is made. If, at the end
|
|
|
+ * of the timeout period, the condition is still false (or failing with
|
|
|
+ * an exception), the timeout handler is invoked, passing in the timeout
|
|
|
+ * and any exception raised in the last invocation. The exception returned
|
|
|
+ * by this timeout handler is then rethrown.
|
|
|
+ * <p>
|
|
|
+ * Example: Wait 30s for a condition to be met, with a sleep of 30s
|
|
|
+ * between each probe.
|
|
|
+ * If the operation is failing, then, after 30s, the timeout handler
|
|
|
+ * is called. This returns the exception passed in (if any),
|
|
|
+ * or generates a new one.
|
|
|
+ * <pre>
|
|
|
+ * await(
|
|
|
+ * 30 * 1000,
|
|
|
+ * () -> { return 0 == filesystem.listFiles(new Path("/")).length); },
|
|
|
+ * () -> 500),
|
|
|
+ * (timeout, ex) -> ex != null ? ex : new TimeoutException("timeout"));
|
|
|
+ * </pre>
|
|
|
+ *
|
|
|
+ * @param timeoutMillis timeout in milliseconds.
|
|
|
+ * Can be zero, in which case only one attempt is made.
|
|
|
+ * @param check predicate to evaluate
|
|
|
+ * @param retry retry escalation logic
|
|
|
+ * @param timeoutHandler handler invoked on timeout;
|
|
|
+ * the returned exception will be thrown
|
|
|
+ * @return the number of iterations before the condition was satisfied
|
|
|
+ * @throws Exception the exception returned by {@code timeoutHandler} on
|
|
|
+ * timeout
|
|
|
+ * @throws FailFastException immediately if the evaluated operation raises it
|
|
|
+ * @throws InterruptedException if interrupted.
|
|
|
+ */
|
|
|
+ public static int await(int timeoutMillis,
|
|
|
+ Callable<Boolean> check,
|
|
|
+ Callable<Integer> retry,
|
|
|
+ TimeoutHandler timeoutHandler)
|
|
|
+ throws Exception {
|
|
|
+ Preconditions.checkArgument(timeoutMillis >= 0,
|
|
|
+ "timeoutMillis must be >= 0");
|
|
|
+ Preconditions.checkNotNull(timeoutHandler);
|
|
|
+
|
|
|
+ long endTime = Time.now() + timeoutMillis;
|
|
|
+ Exception ex = null;
|
|
|
+ boolean running = true;
|
|
|
+ int iterations = 0;
|
|
|
+ while (running) {
|
|
|
+ iterations++;
|
|
|
+ try {
|
|
|
+ if (check.call()) {
|
|
|
+ return iterations;
|
|
|
+ }
|
|
|
+ // the probe failed but did not raise an exception. Reset any
|
|
|
+ // exception raised by a previous probe failure.
|
|
|
+ ex = null;
|
|
|
+ } catch (InterruptedException | FailFastException e) {
|
|
|
+ throw e;
|
|
|
+ } catch (Exception e) {
|
|
|
+ LOG.debug("eventually() iteration {}", iterations, e);
|
|
|
+ ex = e;
|
|
|
+ }
|
|
|
+ running = Time.now() < endTime;
|
|
|
+ if (running) {
|
|
|
+ int sleeptime = retry.call();
|
|
|
+ if (sleeptime >= 0) {
|
|
|
+ Thread.sleep(sleeptime);
|
|
|
+ } else {
|
|
|
+ running = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // timeout
|
|
|
+ Exception evaluate = timeoutHandler.evaluate(timeoutMillis, ex);
|
|
|
+ if (evaluate == null) {
|
|
|
+ // bad timeout handler logic; fall back to GenerateTimeout so the
|
|
|
+ // underlying problem isn't lost.
|
|
|
+ LOG.error("timeout handler {} did not throw an exception ",
|
|
|
+ timeoutHandler);
|
|
|
+ evaluate = new GenerateTimeout().evaluate(timeoutMillis, ex);
|
|
|
+ }
|
|
|
+ throw evaluate;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Simplified {@link #await(int, Callable, Callable, TimeoutHandler)}
|
|
|
+ * operation with a fixed interval
|
|
|
+ * and {@link GenerateTimeout} handler to generate a {@code TimeoutException}.
|
|
|
+ * <p>
|
|
|
+ * Example: await for probe to succeed:
|
|
|
+ * <pre>
|
|
|
+ * await(
|
|
|
+ * 30 * 1000, 500,
|
|
|
+ * () -> { return 0 == filesystem.listFiles(new Path("/")).length); });
|
|
|
+ * </pre>
|
|
|
+ *
|
|
|
+ * @param timeoutMillis timeout in milliseconds.
|
|
|
+ * Can be zero, in which case only one attempt is made.
|
|
|
+ * @param intervalMillis interval in milliseconds between checks
|
|
|
+ * @param check predicate to evaluate
|
|
|
+ * @return the number of iterations before the condition was satisfied
|
|
|
+ * @throws Exception returned by {@code failure} on timeout
|
|
|
+ * @throws FailFastException immediately if the evaluated operation raises it
|
|
|
+ * @throws InterruptedException if interrupted.
|
|
|
+ */
|
|
|
+ public static int await(int timeoutMillis,
|
|
|
+ int intervalMillis,
|
|
|
+ Callable<Boolean> check) throws Exception {
|
|
|
+ return await(timeoutMillis, check,
|
|
|
+ new FixedRetryInterval(intervalMillis),
|
|
|
+ new GenerateTimeout());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Repeatedly execute a closure until it returns a value rather than
|
|
|
+ * raise an exception.
|
|
|
+ * Exceptions are caught and, with one exception,
|
|
|
+ * trigger a sleep and retry. This is similar of ScalaTest's
|
|
|
+ * {@code eventually(timeout, closure)} operation, though that lacks
|
|
|
+ * the ability to fail fast if the inner closure has determined that
|
|
|
+ * a failure condition is non-recoverable.
|
|
|
+ * <p>
|
|
|
+ * Example: spin until an the number of files in a filesystem is non-zero,
|
|
|
+ * returning the files found.
|
|
|
+ * The sleep interval backs off by 500 ms each iteration to a maximum of 5s.
|
|
|
+ * <pre>
|
|
|
+ * FileStatus[] files = eventually( 30 * 1000,
|
|
|
+ * () -> {
|
|
|
+ * FileStatus[] f = filesystem.listFiles(new Path("/"));
|
|
|
+ * assertEquals(0, f.length);
|
|
|
+ * return f;
|
|
|
+ * },
|
|
|
+ * new ProportionalRetryInterval(500, 5000));
|
|
|
+ * </pre>
|
|
|
+ * This allows for a fast exit, yet reduces probe frequency over time.
|
|
|
+ *
|
|
|
+ * @param <T> return type
|
|
|
+ * @param timeoutMillis timeout in milliseconds.
|
|
|
+ * Can be zero, in which case only one attempt is made before failing.
|
|
|
+ * @param eval expression to evaluate
|
|
|
+ * @param retry retry interval generator
|
|
|
+ * @return result of the first successful eval call
|
|
|
+ * @throws Exception the last exception thrown before timeout was triggered
|
|
|
+ * @throws FailFastException if raised -without any retry attempt.
|
|
|
+ * @throws InterruptedException if interrupted during the sleep operation.
|
|
|
+ */
|
|
|
+ public static <T> T eventually(int timeoutMillis,
|
|
|
+ Callable<T> eval,
|
|
|
+ Callable<Integer> retry) throws Exception {
|
|
|
+ Preconditions.checkArgument(timeoutMillis >= 0,
|
|
|
+ "timeoutMillis must be >= 0");
|
|
|
+ long endTime = Time.now() + timeoutMillis;
|
|
|
+ Exception ex;
|
|
|
+ boolean running;
|
|
|
+ int sleeptime;
|
|
|
+ int iterations = 0;
|
|
|
+ do {
|
|
|
+ iterations++;
|
|
|
+ try {
|
|
|
+ return eval.call();
|
|
|
+ } catch (InterruptedException | FailFastException e) {
|
|
|
+ // these two exceptions trigger an immediate exit
|
|
|
+ throw e;
|
|
|
+ } catch (Exception e) {
|
|
|
+ LOG.debug("evaluate() iteration {}", iterations, e);
|
|
|
+ ex = e;
|
|
|
+ }
|
|
|
+ running = Time.now() < endTime;
|
|
|
+ if (running && (sleeptime = retry.call()) >= 0) {
|
|
|
+ Thread.sleep(sleeptime);
|
|
|
+ }
|
|
|
+ } while (running);
|
|
|
+ // timeout. Throw the last exception raised
|
|
|
+ throw ex;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Simplified {@link #eventually(int, Callable, Callable)} method
|
|
|
+ * with a fixed interval.
|
|
|
+ * <p>
|
|
|
+ * Example: wait 30s until an assertion holds, sleeping 1s between each
|
|
|
+ * check.
|
|
|
+ * <pre>
|
|
|
+ * eventually( 30 * 1000, 1000,
|
|
|
+ * () -> { assertEquals(0, filesystem.listFiles(new Path("/")).length); }
|
|
|
+ * );
|
|
|
+ * </pre>
|
|
|
+ *
|
|
|
+ * @param timeoutMillis timeout in milliseconds.
|
|
|
+ * Can be zero, in which case only one attempt is made before failing.
|
|
|
+ * @param intervalMillis interval in milliseconds
|
|
|
+ * @param eval expression to evaluate
|
|
|
+ * @return result of the first successful invocation of {@code eval()}
|
|
|
+ * @throws Exception the last exception thrown before timeout was triggered
|
|
|
+ * @throws FailFastException if raised -without any retry attempt.
|
|
|
+ * @throws InterruptedException if interrupted during the sleep operation.
|
|
|
+ */
|
|
|
+ public static <T> T eventually(int timeoutMillis,
|
|
|
+ int intervalMillis,
|
|
|
+ Callable<T> eval) throws Exception {
|
|
|
+ return eventually(timeoutMillis, eval,
|
|
|
+ new FixedRetryInterval(intervalMillis));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Intercept an exception; throw an {@code AssertionError} if one not raised.
|
|
|
+ * The caught exception is rethrown if it is of the wrong class or
|
|
|
+ * does not contain the text defined in {@code contained}.
|
|
|
+ * <p>
|
|
|
+ * Example: expect deleting a nonexistent file to raise a
|
|
|
+ * {@code FileNotFoundException}.
|
|
|
+ * <pre>
|
|
|
+ * FileNotFoundException ioe = intercept(FileNotFoundException.class,
|
|
|
+ * () -> {
|
|
|
+ * filesystem.delete(new Path("/missing"), false);
|
|
|
+ * });
|
|
|
+ * </pre>
|
|
|
+ *
|
|
|
+ * @param clazz class of exception; the raised exception must be this class
|
|
|
+ * <i>or a subclass</i>.
|
|
|
+ * @param eval expression to eval
|
|
|
+ * @param <T> return type of expression
|
|
|
+ * @param <E> exception class
|
|
|
+ * @return the caught exception if it was of the expected type
|
|
|
+ * @throws Exception any other exception raised
|
|
|
+ * @throws AssertionError if the evaluation call didn't raise an exception.
|
|
|
+ * The error includes the {@code toString()} value of the result, if this
|
|
|
+ * can be determined.
|
|
|
+ */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ public static <T, E extends Throwable> E intercept(
|
|
|
+ Class<E> clazz,
|
|
|
+ Callable<T> eval)
|
|
|
+ throws Exception {
|
|
|
+ try {
|
|
|
+ T result = eval.call();
|
|
|
+ throw new AssertionError("Expected an exception, got "
|
|
|
+ + robustToString(result));
|
|
|
+ } catch (Throwable e) {
|
|
|
+ if (clazz.isAssignableFrom(e.getClass())) {
|
|
|
+ return (E)e;
|
|
|
+ }
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Intercept an exception; throw an {@code AssertionError} if one not raised.
|
|
|
+ * The caught exception is rethrown if it is of the wrong class or
|
|
|
+ * does not contain the text defined in {@code contained}.
|
|
|
+ * <p>
|
|
|
+ * Example: expect deleting a nonexistent file to raise a
|
|
|
+ * {@code FileNotFoundException} with the {@code toString()} value
|
|
|
+ * containing the text {@code "missing"}.
|
|
|
+ * <pre>
|
|
|
+ * FileNotFoundException ioe = intercept(FileNotFoundException.class,
|
|
|
+ * "missing",
|
|
|
+ * () -> {
|
|
|
+ * filesystem.delete(new Path("/missing"), false);
|
|
|
+ * });
|
|
|
+ * </pre>
|
|
|
+ *
|
|
|
+ * @param clazz class of exception; the raised exception must be this class
|
|
|
+ * <i>or a subclass</i>.
|
|
|
+ * @param contained string which must be in the {@code toString()} value
|
|
|
+ * of the exception
|
|
|
+ * @param eval expression to eval
|
|
|
+ * @param <T> return type of expression
|
|
|
+ * @param <E> exception class
|
|
|
+ * @return the caught exception if it was of the expected type and contents
|
|
|
+ * @throws Exception any other exception raised
|
|
|
+ * @throws AssertionError if the evaluation call didn't raise an exception.
|
|
|
+ * The error includes the {@code toString()} value of the result, if this
|
|
|
+ * can be determined.
|
|
|
+ * @see GenericTestUtils#assertExceptionContains(String, Throwable)
|
|
|
+ */
|
|
|
+ public static <T, E extends Throwable> E intercept(
|
|
|
+ Class<E> clazz,
|
|
|
+ String contained,
|
|
|
+ Callable<T> eval)
|
|
|
+ throws Exception {
|
|
|
+ E ex = intercept(clazz, eval);
|
|
|
+ GenericTestUtils.assertExceptionContains(contained, ex);
|
|
|
+ return ex;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Robust string converter for exception messages; if the {@code toString()}
|
|
|
+ * method throws an exception then that exception is caught and logged,
|
|
|
+ * then a simple string of the classname logged.
|
|
|
+ * This stops a {@code toString()} failure hiding underlying problems.
|
|
|
+ * @param o object to stringify
|
|
|
+ * @return a string for exception messages
|
|
|
+ */
|
|
|
+ private static String robustToString(Object o) {
|
|
|
+ if (o == null) {
|
|
|
+ return NULL_RESULT;
|
|
|
+ } else {
|
|
|
+ try {
|
|
|
+ return o.toString();
|
|
|
+ } catch (Exception e) {
|
|
|
+ LOG.info("Exception calling toString()", e);
|
|
|
+ return o.getClass().toString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns {@code TimeoutException} on a timeout. If
|
|
|
+ * there was a inner class passed in, includes it as the
|
|
|
+ * inner failure.
|
|
|
+ */
|
|
|
+ public static class GenerateTimeout implements TimeoutHandler {
|
|
|
+ private final String message;
|
|
|
+
|
|
|
+ public GenerateTimeout(String message) {
|
|
|
+ this.message = message;
|
|
|
+ }
|
|
|
+
|
|
|
+ public GenerateTimeout() {
|
|
|
+ this("timeout");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Evaluate operation creates a new {@code TimeoutException}.
|
|
|
+ * @param timeoutMillis timeout in millis
|
|
|
+ * @param caught optional caught exception
|
|
|
+ * @return TimeoutException
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public Exception evaluate(int timeoutMillis, Exception caught)
|
|
|
+ throws Exception {
|
|
|
+ String s = String.format("%s: after %d millis", message,
|
|
|
+ timeoutMillis);
|
|
|
+ String caughtText = caught != null
|
|
|
+ ? ("; " + robustToString(caught)) : "";
|
|
|
+
|
|
|
+ return (TimeoutException) (new TimeoutException(s + caughtText)
|
|
|
+ .initCause(caught));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Retry at a fixed time period between calls.
|
|
|
+ */
|
|
|
+ public static class FixedRetryInterval implements Callable<Integer> {
|
|
|
+ private final int intervalMillis;
|
|
|
+ private int invocationCount = 0;
|
|
|
+
|
|
|
+ public FixedRetryInterval(int intervalMillis) {
|
|
|
+ Preconditions.checkArgument(intervalMillis > 0);
|
|
|
+ this.intervalMillis = intervalMillis;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Integer call() throws Exception {
|
|
|
+ invocationCount++;
|
|
|
+ return intervalMillis;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int getInvocationCount() {
|
|
|
+ return invocationCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String toString() {
|
|
|
+ final StringBuilder sb = new StringBuilder(
|
|
|
+ "FixedRetryInterval{");
|
|
|
+ sb.append("interval=").append(intervalMillis);
|
|
|
+ sb.append(", invocationCount=").append(invocationCount);
|
|
|
+ sb.append('}');
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gradually increase the sleep time by the initial interval, until
|
|
|
+ * the limit set by {@code maxIntervalMillis} is reached.
|
|
|
+ */
|
|
|
+ public static class ProportionalRetryInterval implements Callable<Integer> {
|
|
|
+ private final int intervalMillis;
|
|
|
+ private final int maxIntervalMillis;
|
|
|
+ private int current;
|
|
|
+ private int invocationCount = 0;
|
|
|
+
|
|
|
+ public ProportionalRetryInterval(int intervalMillis,
|
|
|
+ int maxIntervalMillis) {
|
|
|
+ Preconditions.checkArgument(intervalMillis > 0);
|
|
|
+ Preconditions.checkArgument(maxIntervalMillis > 0);
|
|
|
+ this.intervalMillis = intervalMillis;
|
|
|
+ this.current = intervalMillis;
|
|
|
+ this.maxIntervalMillis = maxIntervalMillis;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Integer call() throws Exception {
|
|
|
+ invocationCount++;
|
|
|
+ int last = current;
|
|
|
+ if (last < maxIntervalMillis) {
|
|
|
+ current += intervalMillis;
|
|
|
+ }
|
|
|
+ return last;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int getInvocationCount() {
|
|
|
+ return invocationCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String toString() {
|
|
|
+ final StringBuilder sb = new StringBuilder(
|
|
|
+ "ProportionalRetryInterval{");
|
|
|
+ sb.append("interval=").append(intervalMillis);
|
|
|
+ sb.append(", current=").append(current);
|
|
|
+ sb.append(", limit=").append(maxIntervalMillis);
|
|
|
+ sb.append(", invocationCount=").append(invocationCount);
|
|
|
+ sb.append('}');
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * An exception which triggers a fast exist from the
|
|
|
+ * {@link #eventually(int, Callable, Callable)} and
|
|
|
+ * {@link #await(int, Callable, Callable, TimeoutHandler)} loops.
|
|
|
+ */
|
|
|
+ public static class FailFastException extends Exception {
|
|
|
+
|
|
|
+ public FailFastException(String detailMessage) {
|
|
|
+ super(detailMessage);
|
|
|
+ }
|
|
|
+
|
|
|
+ public FailFastException(String message, Throwable cause) {
|
|
|
+ super(message, cause);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Instantiate from a format string.
|
|
|
+ * @param format format string
|
|
|
+ * @param args arguments to format
|
|
|
+ * @return an instance with the message string constructed.
|
|
|
+ */
|
|
|
+ public static FailFastException newInstance(String format, Object...args) {
|
|
|
+ return new FailFastException(String.format(format, args));
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|