package org.dcache.services.info.gathers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Required; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import dmg.cells.nucleus.EnvironmentAware; import dmg.cells.nucleus.UOID; import org.dcache.util.NDC; import org.dcache.services.info.base.StateExhibitor; import org.dcache.services.info.base.StateUpdateManager; import static com.google.common.base.Preconditions.checkState; /** * This thread is responsible for scheduling various data-gathering activity. * Multiple DataGatheringActivity instances can be registered, each will operate * independently. The frequency at which they trigger, or even whether they are * periodic, is completely under the control of the DGA. * <p> * These DataGatheringActivities can (in principle) do anything when * triggered, but will typically send one or more messages to dCache. * * @author Paul Millar <paul.millar@desy.de> */ public class DataGatheringScheduler implements Runnable, EnvironmentAware { private static final long FIVE_MINUTES = 5*60*1000; private static final Logger LOGGER_SCHED = LoggerFactory.getLogger(DataGatheringScheduler.class); private static final Logger LOGGER_RA = LoggerFactory.getLogger(RegisteredActivity.class); private boolean _timeToQuit; private final List<RegisteredActivity> _activity = new ArrayList<>(); private Map<String,Object> _environment; private Iterable<DgaFactoryService> _factories; private StateUpdateManager _sum; private StateExhibitor _exhibitor; private MessageSender _sender; private MessageMetadataRepository<UOID> _repository; private Thread _thread; /** * Class holding a periodically repeated DataGatheringActivity * @author Paul Millar <paul.millar@desy.de> */ private static class RegisteredActivity { /** Min. delay (in ms). We prevent Schedulables from triggering more frequently than this */ private static final long MINIMUM_DGA_DELAY = 50; private final Schedulable _dga; /** The delay until this DataGatheringActivity should be next triggered */ private Date _nextTriggered; /** Whether we should include this activity when scheduling next activity */ private boolean _enabled = true; /** * Create a new PeriodicActvity, with specified DataGatheringActivity, that * is triggered with a fixed period. The initial delay is a randomly chosen * fraction of the period. * @param dga the DataGatheringActivity to be triggered periodically * @param period the period between successive triggering in milliseconds. */ RegisteredActivity(Schedulable dga) { _dga = dga; updateNextTrigger(); } /** * Try to make sure we don't hit the system with lots of queries at the same * time * @param period */ private void updateNextTrigger() { Date nextTrigger = _dga.shouldNextBeTriggered(); if (nextTrigger == null) { LOGGER_RA.error("registered dga returned null Date"); nextTrigger = new Date(System.currentTimeMillis() + FIVE_MINUTES); } else { // Safety! Check we wont trigger too quickly if (nextTrigger.getTime() - System.currentTimeMillis() < MINIMUM_DGA_DELAY) { LOGGER_RA.warn("DGA {} triggering too quickly ({}ms): engaging safety.", _dga, nextTrigger.getTime() - System.currentTimeMillis()); nextTrigger = new Date (System.currentTimeMillis() + MINIMUM_DGA_DELAY); } } _nextTriggered = nextTrigger; } /** * Update this PeriodicActivity so it's trigger time is <i>now</i>. */ public void shouldTriggerNow() { _nextTriggered = new Date(); } /** * Check the status of this activity. If the time has elapsed, * this will cause the DataGatheringActivity to be triggered * and the timer to be reset. * @return true if the DataGatheringActivity was triggered. */ boolean checkAndTrigger(Date now) { if (!_enabled) { return false; } if (now.before(_nextTriggered)) { return false; } NDC.push(_dga.toString()); _dga.trigger(); NDC.pop(); updateNextTrigger(); return true; } /** * Calculate the duration until the event has triggered. * @return duration, in milliseconds, until event or zero if it * should have been triggered already. */ long getDelay() { long delay = _nextTriggered.getTime() - System.currentTimeMillis(); return delay > 0 ? delay : 0; } /** * Return the time this will be next triggered. * @return */ long getNextTriggered() { return _nextTriggered.getTime(); } boolean isEnabled() { return _enabled; } void disable() { _enabled = false; } /** * Enable a periodic activity. */ void enable() { if (!_enabled) { _enabled = true; updateNextTrigger(); } } /** * A human-understandable name for this DGA * @return the underlying DGA's name */ @Override public String toString() { return _dga.toString(); } /** * Render current status into a human-understandable form. * @return single-line String describing current status. */ public String getStatus() { StringBuilder sb = new StringBuilder(); sb.append(this.toString()); sb.append(" ["); sb.append(_enabled ? "enabled" : "disabled"); if (_enabled) { sb.append(String .format(", next %1$.1fs", getDelay() / 1000.0)); } sb.append("]"); return sb.toString(); } } public synchronized void start() { checkState(_thread == null, "DataGatheringScheduler already started"); for (DgaFactoryService factory : _factories) { if (factory instanceof EnvironmentAware) { ((EnvironmentAware)factory).setEnvironment(_environment); } for (Schedulable dga : factory.createDgas(_exhibitor, _sender, _sum, _repository)) { _activity.add(new RegisteredActivity(dga)); } } _thread = new Thread(this); _thread.setName("DGA-Scheduler"); _thread.start(); } @Override public void setEnvironment(Map<String,Object> environment) { _environment = environment; } @Required public void setDgaFactories(Iterable<DgaFactoryService> factories) { _factories = factories; } @Required public void setStateUpdateManager(StateUpdateManager sum) { _sum = sum; } @Required public void setStateExhibitor(StateExhibitor exhibitor) { _exhibitor = exhibitor; } @Required public void setMessageSender(MessageSender sender) { _sender = sender; } @Required public void setMessageMetadataRepository(MessageMetadataRepository<UOID> repository) { _repository = repository; } /** * Main loop for this thread triggering DataGatheringActivity. */ @Override public void run() { long delay; Date now = new Date(); LOGGER_SCHED.debug("DGA Scheduler thread starting."); synchronized (_activity) { do { now.setTime(System.currentTimeMillis()); for (RegisteredActivity pa : _activity) { pa.checkAndTrigger(now); } delay = getWaitTimeout(); try { _activity.wait(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } while (!_timeToQuit); } LOGGER_SCHED.debug("DGA Scheduler thread shutting down."); } /** * Search through out list of activity and find the one that matches this name. * <p> * This method assumes that the current thread already owns the _allActivity * monitor * @param name the name of the activity to fine * @return the corresponding PeriodicActivity object, or null if not found. */ private RegisteredActivity findActivity(String name) { RegisteredActivity foundPA = null; for (RegisteredActivity pa : _activity) { if (pa.toString().equals(name)) { foundPA = pa; break; } } return foundPA; } /** * Enable a data-gathering activity, based on a human-readable name. * @param name - name of the DGA. * @return null if successful or an error message if there was a problem. */ public String enableActivity(String name) { RegisteredActivity pa; boolean haveEnabled = false; synchronized (_activity) { pa = findActivity(name); if (pa != null && !pa._enabled) { pa.enable(); _activity.notify(); haveEnabled = true; } } return haveEnabled ? null : pa == null ? "Unknown DGA " + name : "DGA " + name + " already enabled"; } /** * Disabled a data-gathering activity, based on a human-readable name. * @param name - name of the DGA. * @return null if successful or an error message if there was a problem. */ public String disableActivity(String name) { RegisteredActivity pa; boolean haveDisabled = false; synchronized (_activity) { pa = findActivity(name); if (pa != null && pa._enabled) { pa.disable(); _activity.notify(); haveDisabled = true; } } return haveDisabled ? null : pa == null ? "Unknown DGA " + name : "DGA " + name + " already disabled"; } /** * Trigger a periodic activity right now. * @param name the PeriodicActivity to trigger * @return null if successful, an error message if there was a problem. */ public String triggerActivity(String name) { RegisteredActivity pa; synchronized (_activity) { pa = findActivity(name); if (pa != null) { pa.shouldTriggerNow(); _activity.notify(); } } return pa != null ? null : "Unknown DGA " + name; } /** * Request that this thread sends no more requests * for data. */ public void shutdown() { LOGGER_SCHED.debug("Requesting DGA Scheduler to shutdown."); synchronized (_activity) { _timeToQuit = true; _activity.notify(); } } /** * Calculate the delay, in milliseconds, until the next * PeriodicActivity is to be triggered, or 0 if there is * no registered Schedulable objects. * <p> * <i>NB</i> we assume that the current thread has already obtained the monitor for * _allActivity! * @return delay, in milliseconds, until next trigger or zero if there * is no recorded delay. */ private long getWaitTimeout() { long earliestTrig=0; synchronized (_activity) { for (RegisteredActivity thisPa : _activity) { if (!thisPa.isEnabled()) { continue; } long thisTrig = thisPa.getNextTriggered(); if (thisTrig < earliestTrig || earliestTrig == 0) { earliestTrig = thisTrig; } } } long delay = 0; if (earliestTrig > 0) { delay = earliestTrig - System.currentTimeMillis(); delay = delay < 1 ? 1 : delay; // enforce >1 to distinguish between "should trigger now" and "no registered activity". } return delay; } /** * Return a human-readable list of known activity. * @return */ public List<String> listActivity() { List<String> activityList = new ArrayList<>(); synchronized (_activity) { for (RegisteredActivity thisRa : _activity) { activityList.add(thisRa.getStatus()); } } return activityList; } }