/******************************************************************************* * This file is part of OpenNMS(R). * * Copyright (C) 2009-2011 The OpenNMS Group, Inc. * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc. * * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc. * * OpenNMS(R) is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * OpenNMS(R) 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenNMS(R). If not, see: * http://www.gnu.org/licenses/ * * For more information contact: * OpenNMS(R) Licensing <license@opennms.org> * http://www.opennms.org/ * http://www.opennms.com/ *******************************************************************************/ package org.opennms.netmgt.provision; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.opennms.core.concurrent.PausibleScheduledThreadPoolExecutor; import org.opennms.core.utils.ThreadCategory; /** * This class takes the work out of scheduling and queuing calls from the provisioner. Each provisioning * adapter can extend this class for this functionality. The ProvisioningAdapter Interface API methods are final so that * the child class cannot implement them and override the queuing... for what would be the point. * * To use this class, have your provisioning adapter extend this abstract class. You see that you must * implement abstract methods. * * To change the schedule, override the createScheduleForNode method and return a schedule suitable for * the node. In this base class, the same schedule is used for all nodes. * * This class throws away duplicate node/operation tuples. This way you are guaranteed to only receive one node/operation * from the queue, until it is removed from the queue that is. This is the purpose of the initial delay. It is suspected * that the add/update/delete operations will not have any delay. Since there is only one thread per adapter, you will get * these in the order they're scheduled. Be sure that adds/updates/deletes use the same schedule to insure the proper * order. * * TODO: Add logging * TODO: Verify correct Exception handling * TODO: Write tests (especially the equals method of the NodeOperation for proper queue handling) * * @author <a href="mailto:david@opennms.org">David Hustace</a> * @version $Id: $ */ public abstract class SimpleQueuedProvisioningAdapter implements ProvisioningAdapter { private final AdapterOperationQueue m_operationQueue = new AdapterOperationQueue(); private volatile PausibleScheduledThreadPoolExecutor m_executorService; /** * <p>Constructor for SimpleQueuedProvisioningAdapter.</p> * * @param executorService a {@link org.opennms.core.concurrent.PausibleScheduledThreadPoolExecutor} object. */ protected SimpleQueuedProvisioningAdapter(PausibleScheduledThreadPoolExecutor executorService) { m_executorService = executorService; } /** * <p>Constructor for SimpleQueuedProvisioningAdapter.</p> */ protected SimpleQueuedProvisioningAdapter() { this(createDefaultSchedulerService()); } //final for now private final static PausibleScheduledThreadPoolExecutor createDefaultSchedulerService() { PausibleScheduledThreadPoolExecutor executorService = new PausibleScheduledThreadPoolExecutor(1); return executorService; } /* * (non-Javadoc) * @see org.opennms.netmgt.provision.ProvisioningAdapter#getName() */ /** * <p>getName</p> * * @return a {@link java.lang.String} object. */ public abstract String getName(); /** * Adapters extending this class must implement this method. * * This method is called in the run method of an operation to insure that the adapter is ready * for the operation to run for the associated node. The adapter is responsible for setting the schedule, however, * something could have altered the state of readiness for the provisioning system in the meantime. If this method * returns false, the operation is rescheduled with the and the attempts remaining on the operation are not * decremented. * * @param op a {@link org.opennms.netmgt.provision.SimpleQueuedProvisioningAdapter.AdapterOperation} object. * @return a boolean representing the state of readiness from the underlying system integrated by the * implementing adapter. */ public abstract boolean isNodeReady(AdapterOperation op); /** * The class implements the API and therefore the concrete class implements this method to handle * dequeued operations. The concrete implementation should check the operation type to derive the * its behavior. * * @param op a {@link org.opennms.netmgt.provision.SimpleQueuedProvisioningAdapter.AdapterOperation} object. * @throws org.opennms.netmgt.provision.ProvisioningAdapterException if any. */ protected abstract void processPendingOperationForNode(AdapterOperation op) throws ProvisioningAdapterException; /** * Override this method to change the default schedule * @param adapterOperationType * @return */ AdapterOperationSchedule createScheduleForNode(int nodeId, AdapterOperationType adapterOperationType) { return new AdapterOperationSchedule(adapterOperationType); } final List<AdapterOperation> removeOperationsForNode(Integer nodeId) { return m_operationQueue.dequeueOperationsForNode(nodeId); } /* * (non-Javadoc) * @see org.opennms.netmgt.provision.ProvisioningAdapter#addNode(int) */ /** {@inheritDoc} */ public final void addNode(int nodeId) { AdapterOperation op = new AdapterOperation(Integer.valueOf(nodeId), AdapterOperationType.ADD, createScheduleForNode(nodeId, AdapterOperationType.ADD)); if (m_operationQueue.enqueOperation(nodeId, op)) { op.schedule(m_executorService, true); } else { //TODO: log something } } /* * (non-Javadoc) * @see org.opennms.netmgt.provision.ProvisioningAdapter#updateNode(int) */ /** {@inheritDoc} */ public final void updateNode(int nodeId) { AdapterOperation op = new AdapterOperation(Integer.valueOf(nodeId), AdapterOperationType.UPDATE, createScheduleForNode(nodeId, AdapterOperationType.UPDATE)); if (m_operationQueue.enqueOperation(nodeId, op)) { op.schedule(m_executorService, true); } else { //TODO: log something } } /* * (non-Javadoc) * @see org.opennms.netmgt.provision.ProvisioningAdapter#deleteNode(int) */ /** {@inheritDoc} */ public final void deleteNode(int nodeId) { AdapterOperation op = new AdapterOperation(Integer.valueOf(nodeId), AdapterOperationType.DELETE, createScheduleForNode(nodeId, AdapterOperationType.DELETE)); if (m_operationQueue.enqueOperation(nodeId, op)) { op.schedule(m_executorService, true); } else { //TODO: log something } } /* * (non-Javadoc) * @see org.opennms.netmgt.provision.ProvisioningAdapter#nodeConfigChanged(int) */ /** {@inheritDoc} */ public final void nodeConfigChanged(int nodeId) { AdapterOperation op = new AdapterOperation(Integer.valueOf(nodeId), AdapterOperationType.CONFIG_CHANGE, createScheduleForNode(nodeId, AdapterOperationType.CONFIG_CHANGE)); if (m_operationQueue.enqueOperation(nodeId, op)) { op.schedule(m_executorService, true); } else { //TODO: log something } } /** * (non-Javadoc) * * @see org.opennms.netmgt.provision.ProvisioningAdapter#init() * * Override this implementation if needed. */ public void init() { } public static class AdapterOperationQueue { private final List<AdapterOperationQueueListener> m_listeners = new ArrayList<AdapterOperationQueueListener>(); private final ConcurrentHashMap<Integer,LinkedBlockingQueue<AdapterOperation>> m_mappedQueue; public AdapterOperationQueue() { m_mappedQueue = new ConcurrentHashMap<Integer, LinkedBlockingQueue<AdapterOperation>>(); } public synchronized boolean enqueOperation(Integer nodeId, AdapterOperation op) { //TODO: should implement some logic here about what is currently pending in the queue and whether //or not to enqueue. For example, if an update arrives and an add is still in the queue, //perhaps we should just drop the update. Same consideration for delete, but should remove all //operations for the node like nothing ever happened. if (m_mappedQueue.containsKey(nodeId) && m_mappedQueue.get(nodeId).contains(op)) { return false; } else { if (m_mappedQueue.containsKey(nodeId) && !m_mappedQueue.get(nodeId).contains(op)) { m_mappedQueue.get(nodeId).offer(op); } else { LinkedBlockingQueue<AdapterOperation> queue = new LinkedBlockingQueue<AdapterOperation>(); queue.offer(op); m_mappedQueue.put(nodeId, queue); } synchronized(m_listeners) { for (AdapterOperationQueueListener listener : m_listeners) { listener.onEnqueueOperation(op); } } return true; } } public synchronized void enqueOperations(Integer nodeId, Collection<AdapterOperation> ops) { for (AdapterOperation op : ops) { enqueOperation(nodeId, op); } } public synchronized List<AdapterOperation> dequeueOperationsForNode(Integer nodeId) { List<AdapterOperation> ops = new ArrayList<AdapterOperation>(); m_mappedQueue.get(nodeId).drainTo(ops); synchronized(m_listeners) { for (AdapterOperation op : ops) { for (AdapterOperationQueueListener listener : m_listeners) { listener.onDequeueOperation(op); } } } return ops; } public synchronized boolean dequeueOperationForNode(Integer nodeId, AdapterOperation op) { boolean retval = m_mappedQueue.get(nodeId).remove(op); synchronized(m_listeners) { for (AdapterOperationQueueListener listener : m_listeners) { listener.onDequeueOperation(op); } } return retval; } public synchronized LinkedBlockingQueue<AdapterOperation> getOperationQueueForNode(Integer nodeId) { return m_mappedQueue.get(nodeId); } public void addListener(AdapterOperationQueueListener listener) { synchronized(m_listeners) { m_listeners.add(listener); } } public void removeListener(AdapterOperationQueueListener listener) { synchronized(m_listeners) { m_listeners.add(listener); } } public List<AdapterOperationQueueListener> getListeners() { synchronized(m_listeners) { List<AdapterOperationQueueListener> retval = new ArrayList<AdapterOperationQueueListener>(); for (AdapterOperationQueueListener listener : m_listeners) { retval.add(listener); } return retval; } } } public interface AdapterOperationQueueListener { void onEnqueueOperation(AdapterOperation op); void onDequeueOperation(AdapterOperation op); } /** * Represents a node operation to be queued and scheduled. * * @author <a href="mailto:david@opennms.org">David Hustace</a> * */ class AdapterOperation implements Runnable { private final Integer m_nodeId; private final AdapterOperationType m_type; private AdapterOperationSchedule m_schedule; private final Date m_createTime; public AdapterOperation(Integer nodeId, AdapterOperationType type, AdapterOperationSchedule schedule) { m_nodeId = nodeId; m_type = type; m_schedule = schedule; m_createTime = new Date(); } public Integer getNodeId() { return m_nodeId; } public Date getCreateTime() { return m_createTime; } public AdapterOperationType getType() { return m_type; } public AdapterOperationSchedule getSchedule() { return m_schedule; } /** * Schedules this operation * * @param executor * @param reduceAttempts * @return * Returns a future if scheduled. Returns null if remaining attempts to schedule is < 1 */ ScheduledFuture<?> schedule(ScheduledExecutorService executor, boolean reduceAttempts) { ScheduledFuture<?> future = null; if (reduceAttempts) { if (getSchedule().getAttemptsRemainingAndDecrement() > 0) { future = executor.schedule(this, m_schedule.getInitialDelay(), m_schedule.getUnit()); } } else { future = executor.schedule(this, m_schedule.getInitialDelay(), m_schedule.getUnit()); } return future; } //TODO: Test this behavior with Unit Tests, for sure! @Override public boolean equals(Object operation) { boolean equals = false; if (this == operation) { equals = true; } if (operation == null || (operation.getClass() != this.getClass())) { throw new IllegalArgumentException("the Operation Object passed is either null or of the wrong class"); } if (m_nodeId == ((AdapterOperation)operation).getNodeId() && m_type == ((AdapterOperation)operation).getType()) { equals = true; } return equals; } @Override public String toString() { return "Operation: "+m_type+" on Node: "+m_nodeId; } public void run() { try { if (isNodeReady(this)) { m_operationQueue.dequeueOperationForNode(m_nodeId, this); // Synchronize here so that we can signal any interested classes that this // operation is being executed synchronized(this) { try { processPendingOperationForNode(this); } catch (ProvisioningAdapterException e) { log().warn("Exception thrown during adapter queuing, rescheduling: " + e.getMessage(), e); //reschedule if the adapter throws a provisioning adapter exception schedule(getExecutorService(), true); } finally { this.notifyAll(); } } } else { schedule(getExecutorService(), false); } } catch (Throwable e) { log().error("Unexpected exception during node operation: " + e.getMessage(), e); } } } private static ThreadCategory log() { return ThreadCategory.getInstance(SimpleQueuedProvisioningAdapter.class); } /** * Simple class for handling the scheduling bits for an AdapterOperation * * @author <a href="mailto:david@opennms.org">David Hustace</a> */ static class AdapterOperationSchedule { private static final int DEFAULT_ATTEMPTS = 1; private static final int DEFAULT_INTERVAL = 60; private static final int DEFAULT_INITIAL_DELAY = 300; long m_initialDelay; long m_interval; int m_attemptsRemaining; //never set this to 0, it will never schedule TimeUnit m_unit; public AdapterOperationSchedule(long initialDelay, long interval, int attempts, TimeUnit unit) { if (attempts < 1) { attempts = 1; } m_initialDelay = initialDelay; m_interval = interval; m_attemptsRemaining = attempts; m_unit = unit; } public AdapterOperationSchedule() { this(DEFAULT_INITIAL_DELAY, DEFAULT_INTERVAL, DEFAULT_ATTEMPTS, TimeUnit.SECONDS); } /** * This constructor changes the initial delay for configuration change events to 1 hour * @param type */ public AdapterOperationSchedule(AdapterOperationType type) { this(DEFAULT_INITIAL_DELAY, DEFAULT_INTERVAL, DEFAULT_ATTEMPTS, TimeUnit.SECONDS); if (type == AdapterOperationType.CONFIG_CHANGE) { m_initialDelay = 3600; m_interval = 600; } } public long getInitialDelay() { return m_initialDelay; } public long getInterval() { return m_interval; } public TimeUnit getUnit() { return m_unit; } public int getAttemptsRemaining() { return m_attemptsRemaining; } public int getAttemptsRemainingAndDecrement() { int currentAttemptsRemaining = m_attemptsRemaining--; return currentAttemptsRemaining; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("AdapterOperationSchedule; Initial delay: ") ;sb.append(m_initialDelay); sb.append(", Interval: "); sb.append(m_interval); sb.append(", Attempts: "); sb.append(m_attemptsRemaining); sb.append(", Units: "); sb.append(m_unit); return sb.toString(); } } /** * Since the operations are queued, we need a way of identifying type of provisioning action * happened to create the operation. The adapters will need to know what is the appropriate * action to take. * * @author <a href="mailto:david@opennms.org">David Hustace</a> */ static enum AdapterOperationType { ADD(1, "Add"), UPDATE(2, "Update"), DELETE(3, "Delete"), CONFIG_CHANGE(4, "Configuration Change"); private static final Map<Integer, AdapterOperationType> m_idMap; private static final List<Integer> m_ids; private int m_id; private String m_label; static { m_ids = new ArrayList<Integer>(values().length); m_idMap = new HashMap<Integer, AdapterOperationType>(values().length); for (AdapterOperationType operation : values()) { m_ids.add(operation.getId()); m_idMap.put(operation.getId(), operation); } } private AdapterOperationType(int id, String label) { m_id = id; m_label = label; } private Integer getId() { return m_id; } @Override public String toString() { return m_label; } public static AdapterOperationType get(int id) { if (m_idMap.containsKey(id)) { return m_idMap.get(id); } else { throw new IllegalArgumentException("Cannot create AdapterOperation from unknown ID " + id); } } } PausibleScheduledThreadPoolExecutor getExecutorService() { return m_executorService; } public AdapterOperationQueue getOperationQueue() { return m_operationQueue; } }