/* * 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 gobblin.testing; import java.util.concurrent.TimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Predicates; /** * A helper class to perform a check for a condition with a back-off. Primary use of this is when a tests * needs for something asynchronous to happen, say a service to start. **/ public class AssertWithBackoff { /** the max time in milliseconds to wait for the condition to become true */ private long timeoutMs = 30 * 1000; /** a logger to use for logging waiting, results, etc. */ private Logger log = LoggerFactory.getLogger(AssertWithBackoff.class); /** the number to multiple the sleep after condition failure */ private Optional<Double> backoffFactor = Optional.<Double> absent(); /** the max time to sleep between condition failures; */ private Optional<Long> maxSleepMs = Optional.<Long> absent(); public class EqualsCheck<T> implements Predicate<Void> { private final Predicate<T> eqToExpected; private final String message; private final Function<Void, T> actual; public EqualsCheck(Function<Void, T> actual, T expected, String message) { this.eqToExpected = Predicates.equalTo(expected); this.message = message; this.actual = actual; } @Override public boolean apply(Void input) { T currentValue = this.actual.apply(input); getLogger().debug("checking '" + this.message + "': " + currentValue); return this.eqToExpected.apply(currentValue); } } /** Creates a new instance */ public static AssertWithBackoff create() { return new AssertWithBackoff(); } /** Set the max time in milliseconds to wait for the condition to become true */ public AssertWithBackoff timeoutMs(long assertTimeoutMs) { this.timeoutMs = assertTimeoutMs; if (!this.maxSleepMs.isPresent()) { this.maxSleepMs = Optional.of(getAutoMaxSleep()); } if (!this.backoffFactor.isPresent()) { this.backoffFactor = Optional.of(getAutoBackoffFactor()); } return this; } /** the max time in milliseconds to wait for the condition to become true */ public long getTimeoutMs() { return this.timeoutMs; } /** Set the max time to sleep between condition failures */ public AssertWithBackoff maxSleepMs(long maxSleepMs) { this.maxSleepMs = Optional.of(maxSleepMs); return this; } /** The max time to sleep between condition failures */ public long getMaxSleepMs() { return this.maxSleepMs.or(getAutoMaxSleep()); } /** Set the number to multiple the sleep after condition failure */ public AssertWithBackoff backoffFactor(double backoffFactor) { this.backoffFactor = Optional.of(backoffFactor); return this; } /** The number to multiple the sleep after condition failure */ public double getBackoffFactor() { return this.backoffFactor.or(getAutoBackoffFactor()); } /** Set the logger to use for logging waiting, results, etc. */ public AssertWithBackoff logger(Logger log) { this.log = log; return this; } /** The logger to use for logging waiting, results, etc. */ public Logger getLogger() { return this.log; } private long getAutoMaxSleep() { return this.timeoutMs / 3; } private double getAutoBackoffFactor() { return Math.log(getMaxSleepMs()) / Math.log(5); } /** * Performs a check for a condition with a back-off. Primary use of this is when a tests * needs for something asynchronous to happen, say a service to start. * * @param condition the condition to wait for * @param assertTimeoutMs the max time in milliseconds to wait for the condition to become true * @param message a message to print while waiting for the condition * @param log a logger to use for logging waiting, results * @throws TimeoutException if the condition did not become true in the specified time budget */ public void assertTrue(Predicate<Void> condition, String message) throws TimeoutException, InterruptedException { AssertWithBackoff.assertTrue(condition, getTimeoutMs(), message, getLogger(), getBackoffFactor(), getMaxSleepMs()); } /** * A convenience method for {@link #assertTrue(Predicate, String)} to keep checking until a * certain value until it becomes equal to an expected value. * @param actual a function that checks the actual value * @param expected the expected value * @param message a debugging message **/ public <T> void assertEquals(Function<Void, T> actual, T expected, String message) throws TimeoutException, InterruptedException { assertTrue(new EqualsCheck<>(actual, expected, message), message); } /** * Performs a check for a condition with a back-off. Primary use of this is when a tests * needs for something asynchronous to happen, say a service to start. * * @param condition the condition to wait for * @param assertTimeoutMs the max time in milliseconds to wait for the condition to become true * @param message the message to print while waiting for the condition * @param log the logger to use for logging waiting, results * @param backoffFactor the number to multiple the sleep after condition failure; * @param maxSleepMs the max time to sleep between condition failures; default is * @throws TimeoutException if the condition did not become true in the specified time budget * @throws InterrupedException if the assert gets interrupted while waiting for the condition to * become true. */ public static void assertTrue(Predicate<Void> condition, long assertTimeoutMs, String message, Logger log, double backoffFactor, long maxSleepMs) throws TimeoutException, InterruptedException { long startTimeMs = System.currentTimeMillis(); long endTimeMs = startTimeMs + assertTimeoutMs; long currentSleepMs = 0; boolean done = false; try { while (!done && System.currentTimeMillis() < endTimeMs) { done = condition.apply(null); if (!done) { currentSleepMs = computeRetrySleep(currentSleepMs, backoffFactor, maxSleepMs, endTimeMs); log.debug("Condition check for '" + message + "' failed; sleeping for " + currentSleepMs + "ms"); Thread.sleep(currentSleepMs); } } //one last try if (!done && !condition.apply(null)) { throw new TimeoutException("Timeout waiting for condition '" + message + "'."); } } catch (TimeoutException | InterruptedException e) { //pass through throw e; } catch (RuntimeException e) { throw new RuntimeException("Exception checking condition '" + message + "':" + e, e); } } /** * Computes a new sleep for a retry of a condition check. * * @param currentSleepMs the last sleep duration in milliseconds * @param backoffFactor the factor to multiple currentSleepMs * @param maxSleepMs the maximum allowed sleep in milliseconds * @param endTimeMs the end time based on timeout for the condition to become true * @return the new sleep time which will not exceed maxSleepMs and will also not cause to * overshoot endTimeMs */ public static long computeRetrySleep(long currentSleepMs, double backoffFactor, long maxSleepMs, long endTimeMs) { long newSleepMs = Math.round(currentSleepMs * backoffFactor); if (newSleepMs <= currentSleepMs) { // Prevent infinite loops newSleepMs = currentSleepMs + 1; } long currentTimeMs = System.currentTimeMillis(); newSleepMs = Math.min(Math.min(newSleepMs, maxSleepMs), endTimeMs - currentTimeMs); return newSleepMs; } }