/* * 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.core.internal; import static com.google.common.base.Preconditions.checkNotNull; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.apache.brooklyn.util.JavaGroovyEquivalents; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.flags.FlagUtils; import org.apache.brooklyn.util.core.flags.SetFromFlag; import org.apache.brooklyn.util.exceptions.Exceptions; 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.Preconditions; import com.google.common.util.concurrent.Callables; /** * Simple DSL to repeat a fragment of code periodically until a condition is satisfied. * * In its simplest case, it is passed two {@link groovy.lang.Closure}s / {@link Callable} - * the first is executed, then the second. If the second closure returns false, the loop * is repeated; if true, it 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. * <p> * It is configured in a <em>fluent</em> manner. For example, in Groovy: * <pre> * {@code * Repeater.create("Wait until the Frobnitzer is ready") * .repeat { * status = frobnitzer.getStatus() * } * .until { * status == "Ready" || status == "Failed" * } * .limitIterationsTo(30) * .run() * } * </pre> * * Or in Java: * <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> * * @deprecated since 0.7.0, use {@link org.apache.brooklyn.util.repeat.Repeater} instead */ @Deprecated public class Repeater { // TODO Was converted to Java, from groovy. Needs thorough review and improvements // to use idiomatic java private static final Logger log = LoggerFactory.getLogger(Repeater.class); @SetFromFlag private String description; private Callable<?> body = Callables.returning(null); private Callable<Boolean> exitCondition; @SetFromFlag private Long period = null; @SetFromFlag("timeout") private Long durationLimit = null; private int iterationLimit = 0; private boolean rethrowException = false; private boolean rethrowExceptionImmediately = false; private boolean warnOnUnRethrownException = true; public Repeater() { this(MutableMap.of(), null); } public Repeater(Map<?,?> flags) { this(flags, null); } public Repeater(String description) { this(MutableMap.of(), description); } /** * Construct a new instance of Repeater. * * @param flags can include period, timeout, description * @param description a description of the operation that will appear in debug logs. */ public Repeater(Map<?,?> flags, String description) { setFromFlags(flags); this.description = JavaGroovyEquivalents.elvis(description, this.description, "Repeater"); } public void setFromFlags(Map<?,?> flags) { FlagUtils.setFieldsFromFlags(flags, this); } public static Repeater create() { return create(MutableMap.of()); } public static Repeater create(Map<?,?> flags) { return create(flags, null); } public static Repeater create(String description) { return create(MutableMap.of(), description); } public static Repeater create(Map<?,?> flags, String description) { return new Repeater(flags, description); } /** * Sets the main body of the loop to be a no-op. * * @return {@literal this} to aid coding in a fluent style. */ 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) { Preconditions.checkArgument(period > 0, "period must be positive: %s", period); checkNotNull(unit, "unit must not be null"); this.period = unit.toMillis(period); return this; } /** * @see #every(long, TimeUnit) */ public Repeater every(Duration duration) { Preconditions.checkNotNull(duration, "duration must not be null"); Preconditions.checkArgument(duration.toMilliseconds()>0, "period must be positive: %s", duration); this.period = duration.toMilliseconds(); return this; } public Repeater every(groovy.time.Duration duration) { return every(Duration.of(duration)); } /** * @see #every(long, TimeUnit) * @deprecated specify unit */ public Repeater every(long duration) { return every(duration, TimeUnit.MILLISECONDS); } /** * 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; } /** * 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; } /** * 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. * * @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) { Preconditions.checkArgument(deadline > 0, "deadline must be positive: %s", deadline); Preconditions.checkNotNull(unit, "unit must not be null"); this.durationLimit = unit.toMillis(deadline); return this; } /** * @see #limitTimeTo(long, TimeUnit) */ 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.durationLimit = duration.toMilliseconds(); 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() { 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(period != null, "every() method has not been called to set the loop period time units"); Throwable lastError = null; int iterations = 0; long endTime = -1; if (durationLimit != null) { endTime = System.currentTimeMillis() + durationLimit; } while (true) { 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 true; } else { if (log.isDebugEnabled()) { String msg = String.format("%s: unsatisfied during iteration %s %s", description, iterations, (iterationLimit > 0 ? "(max "+iterationLimit+" attempts)" : "") + (endTime > 0 ? "("+Time.makeTimeStringRounded(endTime - System.currentTimeMillis())+" 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 false; } if (endTime > 0) { if (System.currentTimeMillis() > endTime) { if (log.isDebugEnabled()) log.debug("{}: condition not satisfied and deadline {} passed", description, Time.makeTimeStringRounded(endTime - System.currentTimeMillis())); if (rethrowException && lastError != null) { log.error("{}: error caught checking condition: {}", description, lastError.getMessage()); throw Exceptions.propagate(lastError); } return false; } } Time.sleep(period); } } }