org.apache.brooklyn.util.repeat.Repeater.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.brooklyn.util.repeat.Repeater.java

Source

/*
 * 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.
 * <p>
 * In its simplest case it is passed two {@link Callable} objects, the <em>operation</em>
 * and the <em>condition</em> which are executed in that order. This execution is repeated
 * until the <em>condition</em> 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.
 * <p>
 * <pre>{@code
 * Repeater.create("Wait until the Frobnitzer is ready")
 *     .until(new Callable<Boolean>() {
 *              public Boolean call() {
 *                  String status = frobnitzer.getStatus()
 *                  return "Ready".equals(status) || "Failed".equals(status);
 *              }})
 *     .limitIterationsTo(30)
 *     .run()
 * }</pre>
 * <p>
 * The 
 */
public class Repeater implements Callable<Boolean> {

    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)}.
     * <p>
     * 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<Boolean> 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 <code>Repeater.until(...)</code>.
     */
    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<Integer, Duration>() {
            @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; i < iteration; i++) {
                    result = result.multiply(multiplier);
                    if (finalDelay != null && result.compareTo(finalDelay) > 0)
                        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<Boolean> exitCondition) {
        Preconditions.checkNotNull(exitCondition, "exitCondition must not be null");
        this.exitCondition = exitCondition;
        return this;
    }

    public <T> Repeater until(final T target, final Predicate<T> exitCondition) {
        Preconditions.checkNotNull(exitCondition, "exitCondition must not be null");
        return until(new Callable<Boolean>() {
            @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<Boolean> result = runKeepingError();
        result.checkNoError();
        if (!result.get())
            throw new IllegalStateException(description + " unsatisfied after " + Duration.of(timer));
    }

    public ReferenceWithError<Boolean> 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();
    }

}