/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2006-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.capsd;
import java.lang.reflect.UndeclaredThrowableException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import org.opennms.core.fiber.PausableFiber;
import org.opennms.core.utils.DBUtils;
import org.opennms.core.utils.ThreadCategory;
import org.opennms.netmgt.config.CapsdConfigFactory;
import org.opennms.netmgt.config.DataSourceFactory;
/**
* This class implements a simple scheduler to ensure that Capsd rescans occurs
* at the expected intervals.
*
* @author <a href="mailto:mike@opennms.org">Mike Davidson </a>
* @author <a href="http://www.opennms.org/">OpenNMS </a>
*
*/
final class Scheduler implements Runnable, PausableFiber {
/**
* The prefix for the fiber name.
*/
private static final String FIBER_NAME = "Capsd Scheduler";
/**
* SQL used to retrieve list of nodes from the node table.
*/
private static final String SQL_RETRIEVE_NODES = "SELECT nodeid FROM node WHERE nodetype != 'D'";
/**
* SQL used to retrieve the last poll time for all the managed interfaces
* belonging to a particular node.
*/
private static final String SQL_GET_LAST_POLL_TIME = "SELECT iplastcapsdpoll FROM ipinterface WHERE nodeid=? AND (ismanaged = 'M' OR ismanaged = 'N')";
/**
* Special identifier used in place of a valid node id in order to schedule
* an SMB reparenting using the rescan scheduler.
*/
private static final int SMB_REPARENTING_IDENTIFIER = -1;
/**
* The status for this fiber.
*/
private int m_status;
/**
* The worker thread that executes this instance.
*/
private Thread m_worker;
/**
* List of NodeInfo objects representing each of the nodes in the database
* capability of being scheduled.
*/
private List<NodeInfo> m_knownNodes;
/**
* The configured interval (in milliseconds) between rescans
*/
private long m_interval;
/**
* The configured initial sleep (in milliseconds) prior to scheduling
* rescans
*/
private long m_initialSleep;
/**
* The rescan queue where new RescanProcessor objects are enqueued for
* execution.
*/
private ExecutorService m_rescanQ;
private RescanProcessorFactory m_rescanProcessorFactory;
/**
* This class encapsulates the information about a node necessary to
* schedule the node for rescans.
*/
final class NodeInfo implements Runnable {
int m_nodeId;
Timestamp m_lastScanned;
long m_interval;
boolean m_scheduled;
NodeInfo(int nodeId, Timestamp lastScanned, long interval) {
m_nodeId = nodeId;
m_lastScanned = lastScanned;
m_interval = interval;
m_scheduled = false;
}
NodeInfo(int nodeId, Date lastScanned, long interval) {
m_nodeId = nodeId;
m_lastScanned = new Timestamp(lastScanned.getTime());
m_interval = interval;
m_scheduled = false;
}
boolean isScheduled() {
return m_scheduled;
}
int getNodeId() {
return m_nodeId;
}
Timestamp getLastScanned() {
return m_lastScanned;
}
long getRescanInterval() {
return m_interval;
}
void setScheduled(boolean scheduled) {
m_scheduled = scheduled;
}
void setLastScanned(Date lastScanned) {
m_lastScanned = new Timestamp(lastScanned.getTime());
}
void setLastScanned(Timestamp lastScanned) {
m_lastScanned = lastScanned;
}
boolean timeForRescan() {
if (System.currentTimeMillis() >= (m_lastScanned.getTime() + m_interval))
return true;
else
return false;
}
public void run() {
try {
m_rescanProcessorFactory.createRescanProcessor(getNodeId()).run();
} finally {
setLastScanned(new Date());
setScheduled(false);
}
}
}
/**
* Constructs a new instance of the scheduler.
* @param rescanProcessorFactory TODO
*
*/
Scheduler(ExecutorService rescanQ, RescanProcessorFactory rescanProcessorFactory) throws SQLException {
m_rescanQ = rescanQ;
m_rescanProcessorFactory = rescanProcessorFactory;
m_status = START_PENDING;
m_worker = null;
m_knownNodes = Collections.synchronizedList(new LinkedList<NodeInfo>());
// Get rescan interval from configuration factory
//
m_interval = CapsdConfigFactory.getInstance().getRescanFrequency();
if (log().isDebugEnabled())
log().debug("Scheduler: rescan interval(millis): " + m_interval);
// Get initial rescan sleep time from configuration factory
//
m_initialSleep = CapsdConfigFactory.getInstance().getInitialSleepTime();
if (log().isDebugEnabled())
log().debug("Scheduler: initial rescan sleep time(millis): " + m_initialSleep);
// Schedule SMB Reparenting using special nodeId (-1)
//
// Schedule this node in such a way that it will be
// scheduled immediately and SMB reparenting will take place
Date lastSmbReparenting = new Date();
lastSmbReparenting.setTime(System.currentTimeMillis() - m_interval);
if (log().isDebugEnabled())
log().debug("Scheduler: scheduling SMB reparenting...");
NodeInfo smbInfo = new NodeInfo(SMB_REPARENTING_IDENTIFIER, lastSmbReparenting, m_interval);
m_knownNodes.add(smbInfo);
// Load actual known nodes from the database
//
loadKnownNodes();
if (log().isDebugEnabled())
log().debug("Scheduler: done loading known nodes, node count: " + m_knownNodes.size());
}
private ThreadCategory log() {
return ThreadCategory.getInstance(getClass());
}
/**
* Builds a list of NodeInfo objects representing each of the nodes in the
* database capable of being scheduled for rescan.
*
* @throws SQLException
* if there is a problem accessing the database.
*/
private void loadKnownNodes() throws SQLException {
Connection db = null;
PreparedStatement nodeStmt = null;
PreparedStatement ifStmt = null;
ResultSet rs = null;
ResultSet rset = null;
final DBUtils d = new DBUtils(getClass());
try {
db = DataSourceFactory.getInstance().getConnection();
d.watch(db);
// Prepare SQL statements in advance
//
nodeStmt = db.prepareStatement(SQL_RETRIEVE_NODES);
d.watch(nodeStmt);
ifStmt = db.prepareStatement(SQL_GET_LAST_POLL_TIME);
d.watch(ifStmt);
// Retrieve non-deleted nodes from the node table in the database
//
rs = nodeStmt.executeQuery();
d.watch(rs);
while (rs.next()) {
// Retrieve an interface from the ipInterface table in
// the database for its last polled/scanned time
int nodeId = rs.getInt(1);
ifStmt.setInt(1, nodeId); // set nodeid
if (log().isDebugEnabled())
log().debug("loadKnownNodes: retrieved nodeid " + nodeId + ", now getting last poll time.");
rset = ifStmt.executeQuery();
d.watch(rs);
if (rset.next()) {
Timestamp lastPolled = rset.getTimestamp(1);
if (lastPolled != null && rset.wasNull() == false) {
if (log().isDebugEnabled())
log().debug("loadKnownNodes: adding node " + nodeId + " with last poll time " + lastPolled);
NodeInfo nodeInfo = new NodeInfo(nodeId, lastPolled, m_interval);
m_knownNodes.add(nodeInfo);
}
} else {
if (log().isDebugEnabled())
log().debug("Node w/ nodeid " + nodeId + " has no managed interfaces from which to retrieve a last poll time...it will not be scheduled.");
}
}
} finally {
d.cleanUp();
}
}
/**
* Creates a NodeInfo object representing the specified node and adds it to
* the known node list for scheduling.
*
* @param nodeId
* Id of node to be scheduled
*
* @throws SQLException
* if there is any problem accessing the database
*/
void scheduleNode(int nodeId) throws SQLException {
// Retrieve last poll time for the node from the ipInterface
// table.
Connection db = null;
final DBUtils d = new DBUtils(getClass());
try {
db = DataSourceFactory.getInstance().getConnection();
d.watch(db);
PreparedStatement ifStmt = db.prepareStatement(SQL_GET_LAST_POLL_TIME);
d.watch(ifStmt);
ifStmt.setInt(1, nodeId);
ResultSet rset = ifStmt.executeQuery();
d.watch(rset);
if (rset.next()) {
Timestamp lastPolled = rset.getTimestamp(1);
if (lastPolled != null && rset.wasNull() == false) {
if (log().isDebugEnabled())
log().debug("scheduleNode: adding node " + nodeId + " with last poll time " + lastPolled);
m_knownNodes.add(new NodeInfo(nodeId, lastPolled, m_interval));
}
} else
log().warn("scheduleNode: Failed to retrieve last polled time from database for nodeid " + nodeId);
} finally {
d.cleanUp();
}
}
/**
* Removes the specified node from the known node list.
*
* @param nodeId
* Id of node to be removed.
*/
void unscheduleNode(int nodeId) {
synchronized (m_knownNodes) {
Iterator<NodeInfo> iter = m_knownNodes.iterator();
while (iter.hasNext()) {
NodeInfo nodeInfo = iter.next();
if (nodeInfo.getNodeId() == nodeId) {
log().debug("unscheduleNode: removing node " + nodeId + " from the scheduler.");
m_knownNodes.remove(nodeInfo);
break;
}
}
}
}
/**
* Creates a NodeInfo object representing the specified node and adds it to
* the rescan queue for immediate rescanning.
*
* @param nodeId
* Id of node to be rescanned
*/
void forceRescan(int nodeId) {
try {
m_rescanQ.execute(m_rescanProcessorFactory.createForcedRescanProcessor(nodeId));
} catch (RejectedExecutionException e) {
log().error("forceRescan: Failed to add node " + nodeId + " to the rescan queue.", e);
}
}
/**
* Starts the fiber.
*
* @throws java.lang.IllegalStateException
* Thrown if the fiber is already running.
*/
public synchronized void start() {
if (m_worker != null) throw new IllegalStateException("The fiber has already run or is running");
m_worker = new Thread(this, getName());
m_worker.setDaemon(true);
m_worker.start();
m_status = STARTING;
if (log().isDebugEnabled()) log().debug("Scheduler.start: scheduler started");
}
/**
* Stops the fiber. If the fiber has never been run then an exception is
* generated.
*
* @throws java.lang.IllegalStateException
* Throws if the fiber has never been started.
*/
public synchronized void stop() {
if (m_worker == null)
throw new IllegalStateException("The fiber has never been started");
m_status = STOP_PENDING;
m_worker.interrupt();
log().debug("Scheduler.stop: scheduler stopped");
}
/**
* Pauses the scheduler if it is current running. If the fiber has not been
* run or has already stopped then an exception is generated.
*
* @throws java.lang.IllegalStateException
* Throws if the operation could not be completed due to the
* fiber's state.
*/
public synchronized void pause() {
if (m_worker == null)
throw new IllegalStateException("The fiber has never been started");
if (m_status == STOPPED || m_status == STOP_PENDING)
throw new IllegalStateException("The fiber is not running or a stop is pending");
if (m_status == PAUSED)
return;
m_status = PAUSE_PENDING;
notifyAll();
}
/**
* Resumes the scheduler if it has been paused. If the fiber has not been
* run or has already stopped then an exception is generated.
*
* @throws java.lang.IllegalStateException
* Throws if the operation could not be completed due to the
* fiber's state.
*/
public synchronized void resume() {
if (m_worker == null)
throw new IllegalStateException("The fiber has never been started");
if (m_status == STOPPED || m_status == STOP_PENDING)
throw new IllegalStateException("The fiber is not running or a stop is pending");
if (m_status == RUNNING)
return;
m_status = RESUME_PENDING;
notifyAll();
}
/**
* Returns the current of this fiber.
*
* @return The current status.
*/
public synchronized int getStatus() {
if (m_worker != null && m_worker.isAlive() == false)
m_status = STOPPED;
return m_status;
}
/**
* Returns the name of this fiber.
*
* @return a {@link java.lang.String} object.
*/
public String getName() {
return FIBER_NAME;
}
/**
* The main method of the scheduler. This method is responsible for checking
* the runnable queues for ready objects and then enqueuing them into the
* thread pool for execution.
*/
public void run() {
synchronized (this) {
m_status = RUNNING;
}
if (log().isDebugEnabled())
log().debug("Scheduler.run: scheduler running");
// Loop until a fatal exception occurs or until
// the thread is interrupted.
//
boolean firstPass = true;
for (;;) {
// Status check
//
synchronized (this) {
if (m_status != RUNNING && m_status != PAUSED && m_status != PAUSE_PENDING && m_status != RESUME_PENDING) {
if (log().isDebugEnabled())
log().debug("Scheduler.run: status = " + m_status + ", time to exit");
break;
}
}
// If this is the first pass we want to pause momentarily
// This allows the rest of the background processes to come
// up and stabilize before we start generating events from rescans.
//
if (firstPass) {
firstPass = false;
synchronized (this) {
try {
if (log().isDebugEnabled())
log().debug("Scheduler.run: initial sleep configured for " + m_initialSleep + "ms...sleeping...");
wait(m_initialSleep);
} catch (InterruptedException ex) {
if (log().isDebugEnabled())
log().debug("Scheduler.run: interrupted exception during initial sleep...exiting.");
break; // exit for loop
}
}
}
// iterate over the known node list, add any
// nodes ready for rescan to the rescan queue
// for processing.
//
int added = 0;
synchronized (m_knownNodes) {
if (log().isDebugEnabled())
log().debug("Scheduler.run: iterating over known nodes list to schedule...");
Iterator<NodeInfo> iter = m_knownNodes.iterator();
while (iter.hasNext()) {
NodeInfo node = iter.next();
// Don't schedule if already scheduled
if (node.isScheduled())
continue;
// Don't schedule if its not time for rescan yet
if (!node.timeForRescan())
continue;
// Must be time for a rescan!
//
try {
node.setScheduled(true); // Mark node as scheduled
// Special Case...perform SMB reparenting if nodeid
// of the scheduled node is -1
//
if (node.getNodeId() == SMB_REPARENTING_IDENTIFIER) {
if (log().isDebugEnabled())
log().debug("Scheduler.run: time for reparenting via SMB...");
Connection db = null;
try {
db = DataSourceFactory.getInstance().getConnection();
ReparentViaSmb reparenter = new ReparentViaSmb(db);
try {
reparenter.sync();
} catch (SQLException sqlE) {
log().error("Unexpected database error during SMB reparenting", sqlE);
} catch (Throwable t) {
log().error("Unexpected error during SMB reparenting", t);
}
} catch (SQLException sqlE) {
log().error("Unable to get database connection from the factory.", sqlE);
} finally {
if (db != null) {
try {
db.close();
} catch (Throwable e) {
}
}
}
// Update the schedule information for the SMB
// reparenting node
//
node.setLastScanned(new Date());
node.setScheduled(false);
if (log().isDebugEnabled())
log().debug("Scheduler.run: SMB reparenting completed...");
}
// Otherwise just add the NodeInfo to the queue which will create
// a rescanProcessor and run it
//
else {
if (log().isDebugEnabled())
log().debug("Scheduler.run: adding node " + node.getNodeId() + " to the rescan queue.");
m_rescanQ.execute(node);
added++;
}
} catch (RejectedExecutionException e) {
log().info("Scheduler.schedule: failed to add new node to rescan queue", e);
throw new UndeclaredThrowableException(e);
}
}
}
// Wait for 60 seconds if there were no nodes
// added to the rescan queue during this loop,
// otherwise just start over.
//
synchronized (this) {
if (added == 0) {
try {
wait(60000);
} catch (InterruptedException ex) {
break; // exit for loop
}
}
}
} // end for(;;)
log().debug("Scheduler.run: scheduler exiting, state = STOPPED");
synchronized (this) {
m_status = STOPPED;
}
} // end run
}