package org.dcache.services.info.base; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * This singleton class provides a (best-effort) complete representation of * dCache instance's current state. * <p> * It receives fresh information through the updateState() method, which * accepts a StateUpdate object. StateUpdate objects are created by classes * in the org.dcache.services.info.gathers package, either synchronously (see * DataGatheringScheduler), or by dCache messages received asynchronously * (see MessageHandlerChain). * <p> * There is preliminary support for triggering activity on state changes * through StateWatcher interface. Some class may use this to publish * aggregated (or otherwise, derived) data in a timely fashion (see * org.dcache.services.info.secondaryInfoProvider package). Other classes may * use StateWatcher interface to trigger external events, although future * work may include adding support for asynchronous event-based monitoring * that would interface with, but remain separate from this state. * <p> * The State object allows a visitor pattern, through the StateVisitor * interface. The principle usage of this is to serialise the current content * (see classes under org.dcache.services.info.serialisation package), but * some synchronous classes also use this to build lists from dCache current * state (e.g., to send a message requesting data to each currently known * pool). * * @author Paul Millar <paul.millar@desy.de> */ public class State implements StateCaretaker, StateExhibitor, StateObservatory { /** * Constants used for persistent metadata */ public static final String METADATA_BRANCH_CLASS_KEY = "branch"; public static final String METADATA_BRANCH_IDNAME_KEY = "id"; private static final Logger LOGGER = LoggerFactory.getLogger(State.class); /** * Class member variables... */ /** The root branch of the dCache state */ private final StateComposite _state; /** All registered StateWatchers */ private volatile Collection<StateWatcherInfo> _watchers = new ArrayList<>(); /** Our read/write lock: we use the fair version to reduce the risk of writers constantly blocking a reader */ private final ReadWriteLock _stateRWLock = new ReentrantReadWriteLock(true); private final Lock _stateReadLock = _stateRWLock.readLock(); private final Lock _stateWriteLock = _stateRWLock.writeLock(); // TODO: remove this completely. It's only needed to support derived // metrics private StateUpdateManager _updateManager; public State() { // Build our persistent State metadata tree, with default contents StatePersistentMetadata metadata = new StatePersistentMetadata(); metadata.addDefault(); // Build our top-level immortal StateComposite. _state = new StateComposite(metadata); } /** * Record a new StateUpdateManager. This will be used to enqueue * StateUpdates from secondary information providers. * * @param sum * the StateUpdateManager */ public void setStateUpdateManager(StateUpdateManager sum) { _updateManager = sum; } /** * Discover when next to purge metrics from the dCache state. * * @return the Date when a metric or branch will next need to be expired. */ @Override public Date getEarliestMetricExpiryDate() { Date earliestExpiryDate = _state.getEarliestChildExpiryDate(); if (LOGGER.isTraceEnabled()) { if (earliestExpiryDate == null) { LOGGER.trace("reporting that earliest expiry time is never"); } else { LOGGER.trace("reporting that earliest expiry time is {}s in the future", (earliestExpiryDate.getTime() - System.currentTimeMillis())/1000); } } return earliestExpiryDate; } /** * Update the current dCache state by applying, at most, a single * StateUpdate from a Stack of pending updates. If no updates are needed, * the routine will return quickly, although there is the cost of * entering the Stack's monitor. * <p> * Updating dCache state, by adding additional metrics, has three phases: * <ol> * <li>Compile a StateTransition, describing the changes within the * hierarchy. * <li>Check StateWatchers' StatePathPredicates and triggering those * affected. * <li>Traverse the tree, applying the StateTransition * </ol> */ @Override public void processUpdate(StateUpdate update) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("beginning to process update: \n{}", update.debugInfo()); } // It's just possible that we might be called with null; ignore these // spurious calls. if (update == null) { LOGGER.warn("processUpdate called with null StateUpdate"); return; } if (update.countPurges() == 0 && update.count() == 0) { LOGGER.warn("StateUpdate with zero updates encountered"); return; } StateTransition transition = new StateTransition(); try { _stateReadLock.lock(); /** * Update our new StateTransition based on the StateUpdate. */ try { update.updateTransition(_state, transition); } catch (BadStatePathException e) { LOGGER.error("Error updating state:", e); } LOGGER.trace("checking StateWatchers"); StateUpdate resultingUpdate = checkWatchers(transition); // TODO: don't enqueue the update but merge with existing StateTransition and // look for additional StateWatchers. if (resultingUpdate != null) { _updateManager.enqueueUpdate(resultingUpdate); } } finally { _stateReadLock.unlock(); } applyTransition(transition); } /** * Apply a StateTransition to dCache state. This is the final step in * updating the dCache state where the proposed changes are made * permanent. This requires obtaining a writer-lock on the state. * * @param transition * the StateTransition to apply. */ private void applyTransition(StateTransition transition) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("now applying following transition to state:\n\n{}", transition.dumpContents()); } try { _stateWriteLock.lock(); _state.applyTransition(null, transition); } finally { _stateWriteLock.unlock(); } } /** * For a given a StateTransition, check all registered StateWatchers to * see if they are affected. This is achieved by checking each * StateWatcher's Collection of StatePathPredicates. * <p> * If the StatePathPredicate matches some significant change within the * StateTransition, the corresponding StateWatcher's * <code>trigger()</code> method is called. The trigger method is * provided with a StateUpdate within which it may register additional * metrics. * <p> * * @param transition * The StateTransition to apply * @return a StateUpdate containing new metrics, or null if there are no * new metrics to update. */ @Override public StateUpdate checkWatchers(StateTransition transition) { StateUpdate update = new StateUpdate(); StateExhibitor currentState = this; StateExhibitor futureState = null; for (StateWatcherInfo thisWatcherInfo : _watchers) { StateWatcher thisWatcher = thisWatcherInfo.getWatcher(); if (!thisWatcherInfo.isEnabled()) { LOGGER.trace("skipping disabled watcher {}", thisWatcher); continue; } LOGGER.trace("checking watcher {}", thisWatcher); boolean hasBeenTriggered = false; for (StatePathPredicate thisPredicate : thisWatcher.getPredicate()) { LOGGER.trace("checking watcher {} predicate {}", thisWatcher, thisPredicate); hasBeenTriggered = _state.predicateHasBeenTriggered(null, thisPredicate, transition); if (hasBeenTriggered) { break; // we only need one predicate to match, so quit } // early. } if (hasBeenTriggered) { LOGGER.info("triggering watcher {}", thisWatcher); thisWatcherInfo.incrementCounter(); if (futureState == null) { futureState = new PostTransitionStateExhibitor(currentState, transition); } thisWatcher.trigger(update, currentState, futureState); } } return update.count() > 0 || update.countPurges() > 0 ? update : null; } @Override public void setStateWatchers(List<StateWatcher> watchers) { List<StateWatcherInfo> newList = new ArrayList<>(watchers.size()); for (StateWatcher watcher : watchers) { newList.add(new StateWatcherInfo(watcher)); } _watchers = newList; } /** * Return a String containing a list of all state watchers. * * @param prefix * a String to prefix each line of the output * @return the list of watchers. */ @Override public String[] listStateWatcher() { String[] watchers = new String[_watchers.size()]; int i = 0; for (StateWatcherInfo thisWatcherInfo : _watchers) { watchers[i++] = thisWatcherInfo.toString(); } return watchers; } /** * Enable all registered StateWatchers that match a given name * * @param name * the StateWatcher name to enable. * @return the number of matching entries. */ @Override public int enableStateWatcher(String name) { int count = 0; for (StateWatcherInfo thisWatcherInfo : _watchers) { if (name.equals(thisWatcherInfo.getWatcher().toString())) { thisWatcherInfo.enable(); count++; } } return count; } /** * Disable all registered StateWatchers that match a given name * * @param name * the StateWatcher name to disable. * @return the number of matching entries. */ @Override public int disableStateWatcher(String name) { int count = 0; for (StateWatcherInfo thisWatcherInfo : _watchers) { if (name.equals(thisWatcherInfo.getWatcher().toString())) { thisWatcherInfo.disable(); count++; } } return count; } /** * Check for, and remove, expired (mortal) StateComponents. This will * also remove all ephemeral children of moral StateComposites * (branches). * <p> * The process of removing data from dCache tree is similar to * <code>processUpdateStack()</code> and may trigger StateWatchers. This * method has three phases: * <ol> * <li>Build a StateTransition, describing the affected StateComponents, * <li>Check for, and trigger, affected StateWatchers, * <li>Apply the changes described in the StateTransition * <ol> */ @Override public synchronized void removeExpiredMetrics() { // A quick check before obtaining the lock Date expDate = getEarliestMetricExpiryDate(); if (expDate == null || expDate.after(new Date())) { return; } LOGGER.trace("Building StateTransition for expired StateComponents"); StateTransition transition = new StateTransition(); try { _stateReadLock.lock(); _state.buildRemovalTransition(null, transition, false); StateUpdate resultingUpdate = checkWatchers(transition); // TODO: don't enqueue the update but merge with existing StateTransition and // look for additional StateWatchers. if (resultingUpdate != null) { _updateManager.enqueueUpdate(resultingUpdate); } } finally { _stateReadLock.unlock(); } applyTransition(transition); } /** * Allow an arbitrary algorithm to visit the current dCache state, that * is, to receive call-backs describing the process of walking over the * state and the contents therein. * <p> * The data obtained from a single call of <code>visitState()</code> is * protected from inconsistencies due to data being updated whilst the * iteration is taking place. No such protection is available for * multiple calls to <code>visitState()</code>. * * @param visitor * the algorithm that wishes to visit our current state */ @Override public void visitState(StateVisitor visitor) { LOGGER.trace("visitor {} wishing to visit current state", visitor); try { long beforeLock = System.currentTimeMillis(); LOGGER.trace("visitor {} acquiring read lock", visitor); _stateReadLock.lock(); long afterLock = System.currentTimeMillis(); LOGGER.trace("visitor {} acquired read lock (took {} ms), starting visit.", visitor, (afterLock - beforeLock) / 1000.0); if (visitor.isVisitable(null)) { _state.acceptVisitor(null, visitor); } long afterVisit = System.currentTimeMillis(); LOGGER.trace("visitor {} completed visit (took {} ms), releasing read lock.", visitor, (afterVisit - afterLock) / 1000.0); } finally { _stateReadLock.unlock(); } LOGGER.trace("visitor {} finished.", visitor); } /** * Small, simple class to hold information about our registered * StateWatchers, whether they are enabled and how many times they've * been triggered. */ private static class StateWatcherInfo { StateWatcher _watcher; boolean _isEnabled = true; long _counter; StateWatcherInfo(StateWatcher watcher) { _watcher = watcher; } boolean isEnabled() { return _isEnabled; } void enable() { _isEnabled = true; } void disable() { _isEnabled = false; } void incrementCounter() { _counter++; } StateWatcher getWatcher() { return _watcher; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(_watcher.toString()).append(" "); sb.append("("); sb.append(_isEnabled ? "enabled" : "disabled"); sb.append(", triggered: ").append(_counter); sb.append(")"); return sb.toString(); } } /** * Emit output suitable for the info cell command. * * @param pw */ public void getInfo(PrintWriter pw) { pw.print(listStateWatcher().length); pw.println(" state watchers."); pw.print(_updateManager.countPendingUpdates()); pw.println(" pending updates to state."); } }