/* * ALMA - Atacama Large Millimiter Array * (c) European Southern Observatory, 2002 * Copyright by ESO (in the framework of the ALMA collaboration), * All rights reserved * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA * * Created on May 24, 2007 * */ package alma.acs.logging; import java.util.concurrent.TimeUnit; /** * This class can be used to reduce the repeated execution of any kind of code, * by keeping track of the number of intended executions and the time passed since the last actual execution, * and comparing these against configured values (see details below). * <p> * A reduction of the number of executions can be useful if that code is resource intensive (e.g. DB access, remote calls, logging) * or has other unwanted side effects if executed too often (e.g. annoying the user with too frequent error or confirmation dialogues). * Of course this only makes sense if the nature of the problem allows skipping some of these executions. * <p> * This class is so general that it does not actually execute any code; it simply keeps track of counters and timer. * It can be used directly to manage decisions about skipping or executing some code. * For example, the application can instantiate a <code>RepeatGuard</code> with the timer set to one second. * Then whenever the repetitive action would be called, the application first calls {@link #check()}; * for every timer interval, only the first call to <code>check()</code> will return <code>true</code>, * while the subsequent calls will return <code>false</code>, and the application should skip the repeated block of code. * Alternatively this class can be extended or wrapped to become easier to use for specific purposes. * Then typically the code to be executed (e.g. logging a message) becomes part of the specialized repeat guard class, * as in {@link RepeatGuardLogger}. * <p> * The concept of repeat guards in ACS is discussed at http://almasw.hq.eso.org/almasw/bin/view/ACS/LoggingRepetitionControl * <p> * Repetitions can be reduced using * <ol> * <li>the number of times that code execution should be skipped before the code can be executed again * <li>a timer which allows only one execution per time interval, * no matter how many execution attempts have been made in the meantime, * <li><code>OR</code> combination of the above: either enough attempts were made or enough time has passed, whatever happens first * <li><code>AND</code> combination: enough skipped execution attempts, and enough time passed. * </ol> * see also the {@link Logic} enum. */ public class RepeatGuard { /*** Evaluation logic: TIMER (time based guard), COUNTER (count based guard), AND/OR (conjunction/disjunction or both) */ public enum Logic { AND, OR, TIMER, COUNTER } private Logic evaluationMethod; private int maxRepetitions; private long endTimeNs; private long intervalNs; private int counter; /** * @see #counterAtLastExecution() */ private int counterAtLastExecution; private boolean firstTime = true; /** * Constructor. * * @param interval Time interval (in <code>timeUnit</code> units). * @param timeUnit Time unit of <code>interval</code> parameter. * @param maxRepetitions Maximum number of repetitions. * @param logic Evaluation logic for <code>interval</code> and <code>maxRepetitions</code>. * The logic will be "reduced" automatically if <code>interval</code> or <code>maxRepetitions</code> * have a value <= 0, so as to be based only on the other positive value. * @throws IllegalArgumentException if maxRepetitions <= 0 && interval <= 0 */ public RepeatGuard(long interval, TimeUnit timeUnit, int maxRepetitions, Logic logic) { reset(interval, timeUnit, maxRepetitions, logic); } /** * Constructor, convenience for the above, using {@link Logic.OR} evaluation method * if both <code>interval</code> and <code>maxRepetitions</code> are positive values, * otherwise {@Logic.TIMER} or {@Logic.COUNTER} to make sure that only the respective parameter with a positive value gets used. * @param interval Time interval (in <code>timeUnit</code> units). * @param timeUnit Time unit of <code>interval</code> parameter. * @param maxRepetitions Maximum number of repetitions. * @throws IllegalArgumentException if maxRepetitions <= 0 && interval <= 0 */ public RepeatGuard(long interval, TimeUnit timeUnit, int maxRepetitions) { this(interval, timeUnit, maxRepetitions, Logic.OR); } /** * This method checks if the guarded activity is due for execution, or if it should be skipped instead. * <p> * For the first call, it always returns true. * Later it returns <code>true</code> if the last call to <code>check()</code>was longer ago than the <code>interval</code> * given in the constructor or in the <code>reset</code> methods, * and/or if the internal counter has been incremented more than <code>maxRepetitions</code> * by calls to {@link #increment()} or {@link #checkAndIncrement()}. * @return <code>true</code> if guarded activity should be run, <code>false</code> if it should be skipped. */ public synchronized boolean check() { long now = System.nanoTime(); // first time check always returns true, regardless of counter and timer. Then resets timer and counter. if (firstTime) { firstTime = false; counterAtLastExecution = counter; counter = 0; endTimeNs = now + intervalNs; return true; } switch (evaluationMethod) { case AND: if ((now >= endTimeNs) && (counter >= maxRepetitions)) { counterAtLastExecution = counter; counter = 0; endTimeNs = now + intervalNs; return true; } return false; case OR: if ((now >= endTimeNs) || (counter >= maxRepetitions)) { counterAtLastExecution = counter; counter = 0; endTimeNs = now + intervalNs; return true; } return false; case TIMER: if (now >= endTimeNs) { counterAtLastExecution = counter; counter = 0; while (endTimeNs <= now) { // we may have to "catch up" several timer intervals during which nothing was logged endTimeNs += intervalNs; //endTime + interval instead of now + interval to prevent drift } return true; } return false; case COUNTER: if (counter >= maxRepetitions) { counterAtLastExecution = counter; counter = 0; endTimeNs = now + intervalNs; return true; } return false; default: throw new IllegalStateException("Unexpected value of RepeatGuard#evaluationMethod='" + evaluationMethod.toString() + "' found. Please report this bug to ACS."); } } /** * Increments the counter and checks (see {@link #check()}). * @return <code>true</code> if OK, <code>false</code> if should be guarded * @see #check() */ public synchronized boolean checkAndIncrement() { counter++; return check(); } /** * Resets and reconfigures this guard using the given interval, maxRepetitions, and {@link Logic.OR} logic. * @param interval Time interval (in <code>timeUnit</code> units). * @param timeUnit Time unit of <code>interval</code> parameter. * @param maxRepetitions Maximum number of skipped repetitions. * @throws IllegalArgumentException if maxRepetitions <= 0 && interval <= 0 * @see #RepeatGuard(long, TimeUnit, int, Logic) */ public void reset(long interval, TimeUnit timeUnit, int maxRepetitions) { reset(interval, timeUnit, maxRepetitions, Logic.OR); } /** * Resets and reconfigures logic of guard. * @param interval Time interval (in <code>timeUnit</code> units). * @param timeUnit Time unit of <code>interval</code> parameter. * @param maxRepetitions Maximum number of skipped repetitions. * @param logic Evaluation logic for <code>interval</code> and <code>maxRepetitions</code>. * The logic will be "reduced" automatically if <code>interval</code> or <code>maxRepetitions</code> * have a value <= 0, so as to be based only on the other positive value. * @throws IllegalArgumentException if <code>maxRepetitions <= 0 && interval <= 0</code> or required arg == null */ public synchronized void reset(long interval, TimeUnit timeUnit, int maxRepetitions, Logic logic) { // check parameters if (maxRepetitions <= 0 && interval <= 0) { throw new IllegalArgumentException("maxRepetitions <= 0 && interval <= 0 not allowed."); } if (timeUnit == null && interval > 0) { throw new IllegalArgumentException("A timeUnit must be be specified."); } if (logic == null) { throw new IllegalArgumentException("A 'logic' enum value must be specified."); } // use only maxRepetitions if interval <= 0 if (interval <= 0) { evaluationMethod = Logic.COUNTER; } // use only interval if maxRepetitions <= 0 else if (maxRepetitions <= 0) { evaluationMethod = Logic.TIMER; } else { // use interval and/or maxRepetitions as given by the logic parameter. this.evaluationMethod = logic; } this.intervalNs = ( timeUnit != null ? timeUnit.toNanos(interval) : 0 ); this.maxRepetitions = maxRepetitions; reset(); } /** * Resets this guard without changing the configuration for timer, counter and logic. */ public synchronized void reset() { counter = 0; counterAtLastExecution = 0; firstTime = true; } /** * Increase counter value. */ public synchronized void increment() { counter++; } /** * Get current counter value. * @return current counter value. */ public synchronized int counter() { return counter; } /** * Gets the value of the counter that it had when the {@link #check()} or * {@link #checkAndIncrement()} method returned <code>true</code> the last time, * which corresponds to the number of times the activity was skipped before it got executed. * <p> * Calling this method does not make sense if only timer logic was used (no counters configured nor incremented) */ public synchronized int counterAtLastExecution() { return counterAtLastExecution; } }