/* * ALMA - Atacama Large Millimiter Array * (c) European Southern Observatory, 2011 * 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 */ package alma.acs.alarmsystem.source; import java.util.Collection; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import alma.acs.alarmsystem.source.AlarmQueue.AlarmToQueue; import alma.acs.concurrent.NamedThreadFactory; import alma.acs.concurrent.ThreadLoopRunner; import alma.acs.concurrent.ThreadLoopRunner.CancelableRunnable; import alma.acs.concurrent.ThreadLoopRunner.ScheduleDelayMode; import alma.acs.container.ContainerServicesBase; import alma.acs.logging.AcsLogLevel; /** * The implementation of {@link AlarmSource}. * <P> * Flushing of queued alarms is done by a thread to avoid blocking * the caller if there are too many queued alarms. * There is possible critical race if the user executes a sequence * of queueAlarms()/flushAlarms(), it happens if the queueAlarms() * is called when the thread has not yet finished flushing * the alarms. * This is avoided by copying the alarms in {@link AlarmSourceImpl#queue} * in a temporary immutable vector. * <P> * Instances of {@link AlarmSource} objects should be get with {@link ContainerServices#getAlarmSource()}. * * @author acaproni * */ public class AlarmSourceImpl implements AlarmSource { /** * The task to flush the queue of alarms. * @see AlarmSourceImpl#queuing * @author acaproni */ private class QueueFlusherTask implements Runnable { // The local copy of the alarms to flush private final AlarmToQueue[] alarmsToFlush; /** * Constructor * * @param alarmsToFlush The alarms to flush */ public QueueFlusherTask(AlarmToQueue[] alarmsToFlush) { this.alarmsToFlush=alarmsToFlush; } @Override public void run() { for (AlarmToQueue alarm: alarmsToFlush) { setAlarm(alarm.faultFamily, alarm.faultMember, alarm.faultCode, alarm.getProperties(), alarm.active); } } } /** * The loop to clear alarms on the alarm service, run every second. * <P> * The alarms to clear are inserted in {@link AlarmSourceImpl#alarmsToClean} and must be * cleared if they are in the queue for more then {@link AlarmSourceImpl#ALARM_OSCILLATION_TIME} * seconds. * * @author acaproni * */ private class OscillationTask extends CancelableRunnable { @Override public void run() { for (String key : alarmsToClean.keySet()) { if (shouldTerminate) { return; } Long timestamp=alarmsToClean.get(key); if (timestamp==null) { // The entry has been removed by another task (e.g. raiseAlarm) continue; } if (System.currentTimeMillis()-TimeUnit.SECONDS.toMillis(ALARM_OSCILLATION_TIME)>timestamp) { alarmsToClean.remove(key); internalAlarmClear(key); } } } } /** * To limit the effect of oscillations, publishing of inactive alarms * is delayed by <code>ALARM_OSCILLATION_TIME</code> (in seconds) * <P> * If during this time interval the alarm is activated again then * its temporary deactivation will be skipped. * <P> * Visibility is public for testing purposes. */ public static final int ALARM_OSCILLATION_TIME=1; /** * The object to publish alarms */ private final AlarmSender alarmSender; /** * The container services */ private final ContainerServicesBase containerServices; /** * The alarms sent or terminated. * Using this queue should avoid sending the same information (raise or clear) several times in a row. * Note that this is different from the purpose of {@link #alarmsToClean} which limits the alternation * between raise and clear. */ private final AlarmsMap alarms; /** * The queue of alarms to be sent when the queuing will be disabled`. * @see #queuing */ private final AlarmQueue queue = new AlarmQueue(); /** * When <code>true</code> the alarms are queued instead of being sent * immediately. */ private volatile boolean queuing=false; /** * Enable/disable the sending of alarms. * <P> * When the sending is disabled, new alarms are discarded. */ private volatile boolean enabled=true; /** * The loop to limit clear-raise oscillations, by only clearing alarms that have been inactive for a certain time. * @see #alarmsToClean */ private final ThreadLoopRunner oscillationLoop; /** * The alarms to clean are initially stored in alarmsToClean. * <P> In fact the deletion of alarms is delayed by {@link AlarmSourceImpl#ALARM_OSCILLATION_TIME} * to limit the effect of oscillation. * <P> * The key is the ID of the alarm; the value is the system time at which the alarm * was cleared by the user. */ private final ConcurrentHashMap<String, Long> alarmsToClean = new ConcurrentHashMap<String, Long>(); /** * The executor to schedule the threads for flushing the queue * after a given time interval. * * @see AlarmSource#queueAlarms(long, TimeUnit) */ private final ScheduledExecutorService scheduledExecutor; /** * The future for the thread to flush the queue of alarms * after a given time interval. * <P> * Only one thread can be scheduled by AlarmSource#queueAlarms(long, TimeUnit) */ private ScheduledFuture<?> flusherFuture; /** * Constructor * * @param containerServices The container services */ public AlarmSourceImpl(ContainerServicesBase containerServices) { if (containerServices==null) { throw new IllegalArgumentException("Invalid null ContainerServicesBase"); } this.containerServices=containerServices; alarms=new AlarmsMap(containerServices.getThreadFactory(),containerServices.getLogger()); alarmSender=new AlarmSender(containerServices); // Allocating this optional executor here is cheap enough, because no thread is created, see ThreadPoolExecutor#prestartAllCoreThreads() // The pool size is 1 because we can have only one thread at a time scheduledExecutor = Executors.newScheduledThreadPool(1, containerServices.getThreadFactory()); oscillationLoop = new ThreadLoopRunner( new OscillationTask(), ALARM_OSCILLATION_TIME, TimeUnit.SECONDS, containerServices.getThreadFactory(), containerServices.getLogger(), "alarm_osci"); } @Override public void raiseAlarm(String faultFamily, String faultMember, int faultCode) { raiseAlarm(faultFamily,faultMember,faultCode,null); } @Override public void raiseAlarm(String faultFamily, String faultMember, int faultCode, Properties properties) { if (!enabled) { return; } if (queuing) { synchronized (queue) { queue.add(faultFamily, faultMember, faultCode, properties, true); } return; } String id= buildAlarmID(faultFamily, faultMember, faultCode); alarmsToClean.remove(id); if (!alarms.raise(id)) { alarmSender.sendAlarm(faultFamily, faultMember, faultCode, properties, true); } } /** * The alarm to clear is not sent directly to the alarm service. * It is instead queued in {@link AlarmSourceImpl#alarmsToClean} and * will be cleared by the oscillation loop. */ @Override public void clearAlarm(String faultFamily, String faultMember, int faultCode) { if (!enabled) { return; } if (queuing) { synchronized (queue) { queue.add(faultFamily, faultMember, faultCode, null, false); } return; } String id= buildAlarmID(faultFamily, faultMember, faultCode); alarmsToClean.putIfAbsent(id, System.currentTimeMillis()); } /** * Clear an alarm by sending to the alarm service. * * @param id The id of the alarm */ private void internalAlarmClear(String id) { if (id==null || id.isEmpty()) { throw new IllegalArgumentException("Invalid alarm ID received "+id); } String[] alarmMembers=id.split(":"); if (alarmMembers.length!=3) { containerServices.getLogger().warning("Invalid alarm ID received "+id); } String faultFamily=alarmMembers[0]; String faultMember=alarmMembers[1]; String faultCode=alarmMembers[2]; if (!alarms.clear(id)) { alarmSender.sendAlarm(faultFamily, faultMember, Integer.parseInt(faultCode), false); } } @Override public void setAlarm(String faultFamily, String faultMember, int faultCode, Properties alarmProps, boolean active) { if (active) { raiseAlarm(faultFamily,faultMember,faultCode,alarmProps); } else { clearAlarm(faultFamily,faultMember,faultCode); } } @Override public void setAlarm(String faultFamily, String faultMember, int faultCode, boolean active) { setAlarm(faultFamily, faultMember,faultCode,null,active); } @Override public void terminateAllAlarms() { Collection<String> alarmsToTerminate = alarms.getActiveAlarms(); for (String ID: alarmsToTerminate) { String[] strs=ID.split(":"); clearAlarm(strs[0], strs[1], Integer.parseInt(strs[2])); } } @Override public synchronized void queueAlarms(long delayTime, TimeUnit unit) { queuing=true; // try to change the target run time, if the task hasn't been started. if (flusherFuture!=null && !flusherFuture.isDone()) { flusherFuture.cancel(false); } flusherFuture = scheduledExecutor.schedule(new Runnable() { public void run() { flushAlarms(); } }, delayTime, unit); } @Override public void queueAlarms() { queuing = true; } /** * {@inheritDoc} * <p> * This method gets called both directly for flushing alarms immediately, and also * delayed (new thread from {@link #queueAlarms(long, TimeUnit)}). * <p> * Implementation note: this method is synchronized and calls {@link #setAlarm(String, String, int, Properties, boolean)} * asynchronously in order to not block access to this class for too long. * * @see alma.acs.alarmsystem.source.AlarmSource#flushAlarms() */ @Override public synchronized void flushAlarms() { queuing=false; if (flusherFuture!=null) { flusherFuture.cancel(false); } AlarmToQueue[] temp; synchronized (queue) { if (queue.isEmpty()) { return; } temp = new AlarmToQueue[queue.size()]; queue.values().toArray(temp); queue.clear(); } ThreadFactory tf = new NamedThreadFactory(containerServices.getThreadFactory(), "AlarmQueueFlusher"); tf.newThread(new QueueFlusherTask(temp)).start(); } @Override public void disableAlarms() { enabled=false; } @Override public void enableAlarms() { enabled=true; } /** * Build the alarm ID from the triplet. * <P> * This method ensures that all the triplet are built in the right and consistent * way between the different calls. This is particularly important given * that the ID is the key used in the map. * * @param faultFamily The fault family * @param faultMember The fault member * @param faultCode The fault code * @return The ID of the alarm */ private String buildAlarmID(String faultFamily, String faultMember, int faultCode) { return faultFamily+":"+faultMember+":"+faultCode; } /** * Start the threads. * @see #tearDown() */ public void start() { oscillationLoop.setDelayMode(ScheduleDelayMode.FIXED_DELAY); oscillationLoop.runLoop(); alarms.start(); } @Override public void tearDown() { enabled = false; // take care of alarms that the user queued on purpose scheduledExecutor.shutdown(); flushAlarms(); // take care of alarms that should be cleared but whose clearing got delayed for (String key : alarmsToClean.keySet()) { internalAlarmClear(key); } alarmsToClean.clear(); boolean oscillationLoopShutdownOK; try { oscillationLoopShutdownOK = oscillationLoop.shutdown(1, TimeUnit.SECONDS); } catch (InterruptedException ie) { oscillationLoopShutdownOK = false; } if (!oscillationLoopShutdownOK) { containerServices.getLogger().log(AcsLogLevel.ERROR,"Error shutting down the oscillation timer task with a 2 s timeout."); } alarmSender.close(); alarms.shutdown(); } }