
org.apache.brooklyn.util.repeat.Repeater Maven / Gradle / Ivy
Show all versions of brooklyn-utils-common Show documentation
/*
* 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.brooklyn.util.repeat;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.ReferenceWithError;
import org.apache.brooklyn.util.repeat.Repeater;
import org.apache.brooklyn.util.time.CountdownTimer;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.Callables;
/**
* Simple mechanism to repeat an operation periodically until a condition is satisfied.
*
* In its simplest case it is passed two {@link Callable} objects, the operation
* and the condition which are executed in that order. This execution is repeated
* until the condition returns {@code true}, when the loop finishes. Further customization
* can be applied to set the period between loops and place a maximum limit on how long the
* loop should run for, as well as other timing and delay properties.
*
*
{@code
* Repeater.create("Wait until the Frobnitzer is ready")
* .until(new Callable() {
* public Boolean call() {
* String status = frobnitzer.getStatus()
* return "Ready".equals(status) || "Failed".equals(status);
* }})
* .limitIterationsTo(30)
* .run()
* }
*
* The
*/
public class Repeater implements Callable {
private static final Logger log = LoggerFactory.getLogger(Repeater.class);
/** A small initial duration that something should wait between repeats,
* e.g. when doing {@link #backoffTo(Duration)}.
*
* Chosen to be small enough that a user won't notice at all,
* but we're not going to be chewing up CPU while waiting. */
public static final Duration DEFAULT_REAL_QUICK_PERIOD = Duration.millis(10);
private final String description;
private Callable> body = Callables.returning(null);
private Callable exitCondition;
private Function super Integer,Duration> delayOnIteration = null;
private Duration timeLimit = null;
private int iterationLimit = 0;
private boolean rethrowException = false;
private boolean rethrowExceptionImmediately = false;
private boolean warnOnUnRethrownException = true;
public Repeater() {
this(null);
}
/**
* Construct a new instance of Repeater.
*
* @param description a description of the operation that will appear in debug logs.
*/
public Repeater(String description) {
this.description = description != null ? description : "Repeater";
}
public static Repeater create() {
return create(null);
}
public static Repeater create(String description) {
return new Repeater(description);
}
/**
* Sets the main body of the loop to be a no-op; useful if using {@link #until(Callable)} instead
*
* @return {@literal this} to aid coding in a fluent style.
* @deprecated since 0.7.0 this is no-op, as the repeater defaults to repeating nothing, simply remove the call,
* using just Repeater.until(...)
.
*/
public Repeater repeat() {
return repeat(Callables.returning(null));
}
/**
* Sets the main body of the loop.
*
* @param body a closure or other Runnable that is executed in the main body of the loop.
* @return {@literal this} to aid coding in a fluent style.
*/
public Repeater repeat(Runnable body) {
checkNotNull(body, "body must not be null");
this.body = (body instanceof Callable) ? (Callable>)body : Executors.callable(body);
return this;
}
/**
* Sets the main body of the loop.
*
* @param body a closure or other Callable that is executed in the main body of the loop.
* @return {@literal this} to aid coding in a fluent style.
*/
public Repeater repeat(Callable> body) {
checkNotNull(body, "body must not be null");
this.body = body;
return this;
}
/**
* Set how long to wait between loop iterations.
*
* @param period how long to wait between loop iterations.
* @param unit the unit of measurement of the period.
* @return {@literal this} to aid coding in a fluent style.
*/
public Repeater every(long period, TimeUnit unit) {
return every(Duration.of(period, unit));
}
/**
* Set how long to wait between loop iterations, as a constant function in {@link #delayOnIteration}
*/
public Repeater every(Duration duration) {
Preconditions.checkNotNull(duration, "duration must not be null");
Preconditions.checkArgument(duration.toMilliseconds()>0, "period must be positive: %s", duration);
return delayOnIteration(Functions.constant(duration));
}
public Repeater every(groovy.time.Duration duration) {
return every(Duration.of(duration));
}
/** sets a function which determines how long to delay on a given iteration between checks,
* with 0 being mapped to the initial delay (after the initial check) */
public Repeater delayOnIteration(Function super Integer,Duration> delayFunction) {
Preconditions.checkNotNull(delayFunction, "delayFunction must not be null");
this.delayOnIteration = delayFunction;
return this;
}
/** sets the {@link #delayOnIteration(Function)} function to be an exponential backoff as follows:
* @param initialDelay the delay on the first iteration, after the initial check
* @param multiplier the rate at which to increase the loop delay, must be >= 1
* @param finalDelay an optional cap on the loop delay */
public Repeater backoff(final Duration initialDelay, final double multiplier, @Nullable final Duration finalDelay) {
Preconditions.checkNotNull(initialDelay, "initialDelay");
Preconditions.checkArgument(multiplier>=1.0, "multiplier >= 1.0");
return delayOnIteration(new Function() {
@Override
public Duration apply(Integer iteration) {
/* we iterate because otherwise we risk overflow errors by using multiplier^iteration;
* e.g. with:
* return Duration.min(initialDelay.multiply(Math.pow(multiplier, iteration)), finalDelay); */
Duration result = initialDelay;
for (int i=0; i0)
return finalDelay;
}
return result;
}
});
}
/** convenience to start with a 10ms delay and exponentially back-off at a rate of 1.2
* up to a max per-iteration delay as supplied here.
* 1.2 chosen because it decays nicely, going from 10ms to 1s in approx 25 iterations totalling 5s elapsed time. */
public Repeater backoffTo(final Duration finalDelay) {
return backoff(Duration.millis(10), 1.2, finalDelay);
}
/**
* Set code fragment that tests if the loop has completed.
*
* @param exitCondition a closure or other Callable that returns a boolean. If this code returns {@literal true} then the
* loop will stop executing.
* @return {@literal this} to aid coding in a fluent style.
*/
public Repeater until(Callable exitCondition) {
Preconditions.checkNotNull(exitCondition, "exitCondition must not be null");
this.exitCondition = exitCondition;
return this;
}
public Repeater until(final T target, final Predicate exitCondition) {
Preconditions.checkNotNull(exitCondition, "exitCondition must not be null");
return until(new Callable() {
@Override
public Boolean call() throws Exception {
return exitCondition.apply(target);
}
});
}
/**
* If the exit condition check throws an exception, it will be recorded and the last exception will be thrown on failure.
*
* @return {@literal this} to aid coding in a fluent style.
*/
public Repeater rethrowException() {
this.rethrowException = true;
return this;
}
/**
* If the repeated body or the exit condition check throws an exception, then propagate that exception immediately.
*
* @return {@literal this} to aid coding in a fluent style.
*/
public Repeater rethrowExceptionImmediately() {
this.rethrowExceptionImmediately = true;
return this;
}
public Repeater suppressWarnings() {
this.warnOnUnRethrownException = false;
return this;
}
/**
* Set the maximum number of iterations.
*
* The loop will exit if the condition has not been satisfied after this number of iterations.
*
* @param iterationLimit the maximum number of iterations.
* @return {@literal this} to aid coding in a fluent style.
*/
public Repeater limitIterationsTo(int iterationLimit) {
Preconditions.checkArgument(iterationLimit > 0, "iterationLimit must be positive: %s", iterationLimit);
this.iterationLimit = iterationLimit;
return this;
}
/**
* @see #limitTimeTo(Duration)
*
* @param deadline the time that the loop should wait.
* @param unit the unit of measurement of the period.
* @return {@literal this} to aid coding in a fluent style.
*/
public Repeater limitTimeTo(long deadline, TimeUnit unit) {
return limitTimeTo(Duration.of(deadline, unit));
}
/**
* Set the amount of time to wait for the condition.
* The repeater will wait at least this long for the condition to be true,
* and will exit soon after even if the condition is false.
*/
public Repeater limitTimeTo(Duration duration) {
Preconditions.checkNotNull(duration, "duration must not be null");
Preconditions.checkArgument(duration.toMilliseconds() > 0, "deadline must be positive: %s", duration);
this.timeLimit = duration;
return this;
}
/**
* Run the loop.
*
* @return true if the exit condition was satisfied; false if the loop terminated for any other reason.
*/
public boolean run() {
return runKeepingError().getWithoutError();
}
public void runRequiringTrue() {
Stopwatch timer = Stopwatch.createStarted();
ReferenceWithError result = runKeepingError();
result.checkNoError();
if (!result.get())
throw new IllegalStateException(description+" unsatisfied after "+Duration.of(timer));
}
public ReferenceWithError runKeepingError() {
Preconditions.checkState(body != null, "repeat() method has not been called to set the body");
Preconditions.checkState(exitCondition != null, "until() method has not been called to set the exit condition");
Preconditions.checkState(delayOnIteration != null, "every() method (or other delaySupplier() / backoff() method) has not been called to set the loop delay");
Throwable lastError = null;
int iterations = 0;
CountdownTimer timer = timeLimit!=null ? CountdownTimer.newInstanceStarted(timeLimit) : CountdownTimer.newInstancePaused(Duration.PRACTICALLY_FOREVER);
while (true) {
Duration delayThisIteration = delayOnIteration.apply(iterations);
iterations++;
try {
body.call();
} catch (Exception e) {
log.warn(description, e);
if (rethrowExceptionImmediately) throw Exceptions.propagate(e);
}
boolean done = false;
try {
lastError = null;
done = exitCondition.call();
} catch (Exception e) {
if (log.isDebugEnabled()) log.debug(description, e);
lastError = e;
if (rethrowExceptionImmediately) throw Exceptions.propagate(e);
}
if (done) {
if (log.isDebugEnabled()) log.debug("{}: condition satisfied", description);
return ReferenceWithError.newInstanceWithoutError(true);
} else {
if (log.isDebugEnabled()) {
String msg = String.format("%s: unsatisfied during iteration %s %s", description, iterations,
(iterationLimit > 0 ? "(max "+iterationLimit+" attempts)" : "") +
(timer.isRunning() ? "("+Time.makeTimeStringRounded(timer.getDurationRemaining())+" remaining)" : ""));
if (iterations == 1) {
log.debug(msg);
} else {
log.trace(msg);
}
}
}
if (iterationLimit > 0 && iterations >= iterationLimit) {
if (log.isDebugEnabled()) log.debug("{}: condition not satisfied and exceeded iteration limit", description);
if (rethrowException && lastError != null) {
log.warn("{}: error caught checking condition (rethrowing): {}", description, lastError.getMessage());
throw Exceptions.propagate(lastError);
}
if (warnOnUnRethrownException && lastError != null)
log.warn("{}: error caught checking condition: {}", description, lastError.getMessage());
return ReferenceWithError.newInstanceMaskingError(false, lastError);
}
if (timer.isExpired()) {
if (log.isDebugEnabled()) log.debug("{}: condition not satisfied, with {} elapsed (limit {})",
new Object[] { description, Time.makeTimeStringRounded(timer.getDurationElapsed()), Time.makeTimeStringRounded(timeLimit) });
if (rethrowException && lastError != null) {
log.error("{}: error caught checking condition: {}", description, lastError.getMessage());
throw Exceptions.propagate(lastError);
}
return ReferenceWithError.newInstanceMaskingError(false, lastError);
}
Time.sleep(delayThisIteration);
}
}
public String getDescription() {
return description;
}
public Duration getTimeLimit() {
return timeLimit;
}
@Override
public Boolean call() throws Exception {
return run();
}
}