package org.dcache.services.info.gathers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; import java.util.Set; import java.util.Stack; import org.dcache.services.info.base.StateExhibitor; import org.dcache.services.info.base.StatePath; import org.dcache.services.info.stateInfo.ListVisitor; /** * Provide generic support for building a list of items, taken from some part of the current * dCache state and triggering some activity based on this list. Most likely this is * sending off messages, but it could (in principle) be anything. * <p> * Events will be triggered for each list item before the current state is re-evaluated and a * fresh list generated. The method getNextItem() treats the List as a stack and pops the next * item off the list. It is assumed (although not required) that subclasses will call getNextItem() * once per trigger. * <p> * The time between successive trigger()s is carefully controlled. If there are items on the * list, then SUCCESSIVE_MSG_DELAY milliseconds is used. Once the list is exhausted, it is * automatically refreshed when triggered() is next called. If the list is empty, then the next * trigger may be subject to an additional delay. This is to ensure that the list is generated * no more than once every MINIMUM_LIST_REFRESH_PERIOD milliseconds, which is to ensure that no * element is called more than once in this period. * * @author Paul Millar <paul.millar@desy.de> */ public abstract class SkelListBasedActivity implements Schedulable { private static final Logger LOGGER = LoggerFactory.getLogger(SkelListBasedActivity.class); /** Minimum time between fetching a fresh list (or querying the same list-item), in milliseconds */ private static final int MINIMUM_LIST_REFRESH_PERIOD = 60000; /** Time between sending successive messages, in milliseconds */ private static final int SUCCESSIVE_MSG_DELAY = 100; /** For how long should the resulting metrics live? (in seconds) */ private long _metricLifetime; /** When it is acceptable to refresh the list, based on the MINIMUM_LIST_REFRESH_PERIOD */ private Date _whenListRefresh; /** When we should next send a message */ private Date _nextSendMsg = new Date(); /** Our collection of to-be-done work */ private final Stack<String> _outstandingWork = new Stack<>(); /** The StatePath pointing to the parent who's children we want to iterate over */ private final StatePath _parentPath; /** Minimum time between fetching a fresh list (or querying the same list-item), in milliseconds */ private final int _minimumListRefreshPeriod; /** Time between sending successive messages, in milliseconds */ private final int _successiveMsgDelay; private final StateExhibitor _exhibitor; /** * Create a new List-based activity, based on the list of items below parentPath in * dCache's state. * @param parentPath all list items must satisfy parentPath.isParentOf(item) */ protected SkelListBasedActivity (StateExhibitor exhibitor, StatePath parentPath) { _parentPath = parentPath; _exhibitor = exhibitor; updateStack(); // Bring in initial work. _minimumListRefreshPeriod = MINIMUM_LIST_REFRESH_PERIOD; _successiveMsgDelay = SUCCESSIVE_MSG_DELAY; randomiseDelay(); // Randomise our initial offset. } /** * Create a new List-based activity that is based on a list of items below parentPath * in dCache's state. The trigger() method is called periodically and getNextItem() method * provides the name of the item to be processed. * @param parentPath the path of the parent object we should iterate over * @param minimumListRefreshPeriod An enforced minimum time, in milliseconds, between the same item being called. * @param successiveMsgDelay The minimum time between triggering() successive items, in milliseconds. */ protected SkelListBasedActivity (StateExhibitor exhibitor, StatePath parentPath, int minimumListRefreshPeriod, int successiveMsgDelay) { _parentPath = parentPath; _exhibitor = exhibitor; updateStack(); // Bring in initial work. _minimumListRefreshPeriod = minimumListRefreshPeriod; _successiveMsgDelay = successiveMsgDelay; randomiseDelay(); // Randomise our initial offset. } /** * Set when we are next to trigger an event to be a random fraction of * the minimum refresh period. This is most useful when starting up. */ private void randomiseDelay() { _whenListRefresh = new Date(System.currentTimeMillis() + (long) (Math.random() * _minimumListRefreshPeriod)); } /** * When should we next be triggered? * @returns the desired time we should next be triggered. */ @Override public Date shouldNextBeTriggered() { return _outstandingWork.empty() ? _whenListRefresh : _nextSendMsg; } /** * We maintain our outstanding work List by (potentially) fetching a new list from the * current state. Classes that extend this class should call super() so getNextItem() * continues to work. */ @Override public void trigger() { Date now = new Date(); _nextSendMsg = new Date(System.currentTimeMillis() + _successiveMsgDelay); if (!_outstandingWork.empty() || now.before(_whenListRefresh)) { return; } updateStack(); /** * Calculate the earliest we would like to do this again. */ long timeToSendAllMsgs = (long) (_outstandingWork.size() * _successiveMsgDelay); long listRefreshPeriod = timeToSendAllMsgs < _minimumListRefreshPeriod ? _minimumListRefreshPeriod : timeToSendAllMsgs; _whenListRefresh = new Date(System.currentTimeMillis() + listRefreshPeriod); /** * All metrics that are generated should have a lifetime based on when we expect * to refresh the list and generate more metrics. * The 2.5 factor allows for both 50% growth and a message being lost. */ _metricLifetime = (long) (2.5 * listRefreshPeriod / 1000.0); } /** * Query dCache's current state and add the new Set to to our _outstandingWork Stack. */ private void updateStack() { Set<String> items = ListVisitor.getDetails(_exhibitor, _parentPath); for (String item : items) { _outstandingWork.add(item); } LOGGER.trace("fresh to-do list obtained for {}", getClass().getSimpleName()); LOGGER.trace("list now contains {} items", _outstandingWork.size()); } /** * @return the next item off the list if there is one, or null otherwise. */ public String getNextItem() { if (_outstandingWork.empty()) { return null; } return _outstandingWork.pop(); } /** * @return an appropriate lifetime for a metric, in seconds. */ public long getMetricLifetime() { return _metricLifetime; } }