/*******************************************************************************
* 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.linkd.scheduler;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.opennms.core.concurrent.LogPreservingThreadFactory;
import org.opennms.core.fiber.PausableFiber;
import org.opennms.core.queue.FifoQueueImpl;
import org.opennms.core.utils.LogUtils;
/**
* This class implements a simple scheduler to ensure the polling occurs at the
* expected intervals. The scheduler employees a dynamic thread pool that adjust
* to the load until a maximum thread count is reached.
*/
public class Scheduler implements Runnable, PausableFiber, ScheduleTimer {
/**
* The map of queue that contain {@link ReadyRunnable ready runnable}
* instances. The queues are mapped according to the interval of scheduling.
*/
public Map<Long,PeekableFifoQueue<ReadyRunnable>> m_queues;
/**
* The total number of elements currently scheduled. This should be the sum
* of all the elements in the various queues.
*/
public int m_scheduled;
/**
* The pool of threads that are used to executed the runnable instances
* scheduled by the class' instance.
*/
public ExecutorService m_runner;
/**
* The status for this fiber.
*/
public int m_status;
/**
* The worker thread that executes this instance.
*/
private Thread m_worker;
/**
* This queue extends the standard FIFO queue instance so that it is
* possible to peek at an instance without removing it from the queue.
*
*/
public static final class PeekableFifoQueue<T> extends FifoQueueImpl<T> {
/**
* This method allows the caller to peek at the next object that would
* be returned on a <code>remove</code> call. If the queue is
* currently empty then the caller is blocked until an object is put
* into the queue.
*
* @return The object that would be returned on the next call to
* <code>remove</code>.
*
* @throws java.lang.InterruptedException
* Thrown if the thread is interrupted.
* @throws org.opennms.core.queue.FifoQueueException
* Thrown if an error occurs removing an item from the
* queue.
*/
public T peek() throws InterruptedException {
return m_delegate.peek();
}
}
/**
* Constructs a new instance of the scheduler. The maximum number of
* executable threads is specified in the constructor. The executable
* threads are part of a runnable thread pool where the scheduled runnables
* are executed.
*
* @param parent
* String prepended to "Scheduler" to create fiber name
* @param maxSize
* The maximum size of the thread pool.
*/
public Scheduler(String parent, int maxSize) {
m_status = START_PENDING;
m_runner = Executors.newFixedThreadPool(
maxSize,
new LogPreservingThreadFactory(getClass().getSimpleName(), maxSize, false)
);
m_queues = new ConcurrentSkipListMap<Long,PeekableFifoQueue<ReadyRunnable>>();
m_scheduled = 0;
m_worker = null;
}
/**
* Constructs a new instance of the scheduler. The maximum number of
* executable threads is specified in the constructor. The executable
* threads are part of a runnable thread pool where the scheduled runnables
* are executed.
*
* @param parent
* String prepended to "Scheduler" to create fiber name
* @param maxSize
* The maximum size of the thread pool.
* @param lowMark
* The low water mark ratios of thread size to threads when
* threads are stopped.
* @param hiMark
* The high water mark ratio of thread size to threads when
* threads are started.
*/
public Scheduler(String parent, int maxSize, float lowMark, float hiMark) {
this(parent, maxSize);
}
/**
* This method is used to schedule a ready runnable in the system. The
* interval is used as the key for determining which queue to add the
* runnable.
*
* @param runnable
* The element to run when interval expires.
* @param interval
* The queue to add the runnable to.
* @throws java.lang.RuntimeException
* Thrown if an error occurs adding the element to the queue.
*/
public synchronized void schedule(ReadyRunnable runnable, long interval) {
LogUtils.debugf(this, "schedule: Adding ready runnable at interval %d", interval);
if (!m_queues.containsKey(interval)) {
LogUtils.debugf(this, "schedule: interval queue did not exist, a new one has been created");
m_queues.put(interval, new PeekableFifoQueue<ReadyRunnable>());
}
try {
(m_queues.get(interval)).add(runnable);
if (m_scheduled++ == 0) {
LogUtils.debugf(this, "schedule: queue element added, calling notify all since none were scheduled");
notifyAll();
} else {
LogUtils.debugf(this, "schedule: queue element added, notification not performed");
}
} catch (InterruptedException ie) {
LogUtils.infof(this, ie, "schedule: failed to add new ready runnable instance %s to scheduler", runnable);
Thread.currentThread().interrupt();
}
}
/**
* {@inheritDoc}
*
* This method is used to schedule a ready runnable in the system. The
* interval is used as the key for determining which queue to add the
* runnable.
*/
public synchronized void schedule(long interval,
final ReadyRunnable runnable) {
final long timeToRun = getCurrentTime() + interval;
ReadyRunnable timeKeeper = new ReadyRunnable() {
public boolean isReady() {
return getCurrentTime() >= timeToRun && runnable.isReady();
}
public String getInfo() {
return runnable.getInfo();
}
public void run() {
runnable.run();
}
public void schedule() {
runnable.schedule();
}
public void suspend() {
runnable.suspend();
}
public void wakeUp() {
runnable.wakeUp();
}
public boolean isSuspended() {
return runnable.isSuspended();
}
public boolean equals(ReadyRunnable r) {
return runnable.equals(r);
}
public void unschedule() {
runnable.unschedule();
}
public String toString() {
return runnable.toString() + " (ready in "
+ Math.max(0, timeToRun - getCurrentTime()) + "ms)";
}
public String getPackageName() {
return runnable.getPackageName();
}
public void setPackageName(String pkg) {
runnable.setPackageName(pkg);
}
};
schedule(timeKeeper, interval);
}
/**
* This method is used to unschedule a ready runnable in the system.
* The runnuble is removed from all queue interval where is found.
*
* @param runnable
* The element to remove from queue intervals.
*/
public synchronized void unschedule(ReadyRunnable runnable) {
LogUtils.debugf(this, "unschedule: Removing all %s", runnable.getInfo());
boolean done = false;
synchronized(m_queues) {
Iterator<Long> iter = m_queues.keySet().iterator();
while (iter.hasNext() && !done) {
Long key = iter.next();
unschedule(runnable, key.longValue());
}
}
}
/**
* This method is used to unschedule a ready runnable in the system. The
* interval is used as the key for determining which queue to remove the
* runnable.
*
* @param interval
* The queue to add the runnable to.
* @param runnable
* The element to remove.
*/
public synchronized void unschedule(ReadyRunnable runnable, long interval) {
LogUtils.debugf(this, "unschedule: Removing %s at interval %d", runnable.getInfo(), interval);
synchronized(m_queues) {
if (!m_queues.containsKey(interval)) {
LogUtils.debugf(this, "unschedule: interval queue did not exist, exit");
return;
}
PeekableFifoQueue<ReadyRunnable> in = m_queues.get(interval);
if (in.isEmpty()) {
LogUtils.debugf(this, "unschedule: interval queue is empty, exit");
return;
}
ReadyRunnable readyRun = null;
int maxLoops = in.size();
boolean first = true;
do {
try {
readyRun = in.remove();
if (in.size() == maxLoops && first) {
maxLoops++;
}
first = false;
if (readyRun != null && readyRun.equals(runnable)) {
LogUtils.debugf(this, "unschedule: removing found %s", readyRun.getInfo());
// Pop the interface/readyRunnable from the
// queue for execution.
//
m_scheduled--;
} else {
in.add(readyRun);
}
} catch (InterruptedException ie) {
LogUtils.infof(this, ie, "unschedule: failed to remove instance %s from scheduler", runnable.getInfo());
Thread.currentThread().interrupt();
}
} while ( --maxLoops > 0) ;
}
}
/**
* This method is used to get a ready runnable in the system.
*
* @param runnable
* The element to get from queues interval.
* @return a {@link org.opennms.netmgt.linkd.scheduler.ReadyRunnable} object.
*/
public synchronized ReadyRunnable getReadyRunnable(ReadyRunnable runnable) {
LogUtils.debugf(this, "getReadyRunnable: Retrieving %s", runnable.getInfo());
ReadyRunnable rr = null;
synchronized (m_queues) {
// get an iterator so that we can cycle
// through the queue elements.
//
Iterator<Long> iter = m_queues.keySet().iterator();
while (iter.hasNext() && rr==null) {
Long interval = iter.next();
rr = getReadyRunnable(runnable, interval.longValue());
}
}
if (rr == null) {
LogUtils.infof(this, "getReadyRunnable: instance %s not found on scheduler", runnable.getInfo());
}
return rr;
}
/**
* <p>getReadyRunnable</p>
*
* @param runnable a {@link org.opennms.netmgt.linkd.scheduler.ReadyRunnable} object.
* @param interval a long.
* @return a {@link org.opennms.netmgt.linkd.scheduler.ReadyRunnable} object.
*/
public synchronized ReadyRunnable getReadyRunnable(ReadyRunnable runnable, long interval) {
LogUtils.debugf(this, "getReadyRunnable: Retrieving %s at interval %d", runnable.getInfo(), interval);
if (!m_queues.containsKey(interval)) {
LogUtils.debugf(this, "getReadyRunnable: interval queue did not exist, exit");
return null;
}
ReadyRunnable rr = null;
synchronized (m_queues) {
PeekableFifoQueue<ReadyRunnable> in = m_queues.get(interval);
if (in.isEmpty()) {
LogUtils.warnf(this, "getReadyRunnable: queue is Empty");
return null;
}
int maxLoops = in.size();
ReadyRunnable readyRun = null;
boolean first = true;
do {
try {
readyRun = in.remove();
if (in.size() == maxLoops && first) {
maxLoops++;
}
first = false;
if (readyRun != null && readyRun.equals(runnable)) {
LogUtils.debugf(this, "getReadyRunnable: found ready runnable %s", readyRun);
rr = readyRun;
}
in.add(readyRun);
} catch (InterruptedException ie) {
LogUtils.infof(this, ie, "getReadyRunnable: failed to get instance %s from scheduler", readyRun.getInfo());
Thread.currentThread().interrupt();
}
} while (--maxLoops > 0) ;
}
if (rr == null) {
LogUtils.infof(this, "getReadyRunnable: instance %s not found on scheduler", runnable.getInfo());
}
return rr;
}
/**
* This returns the current time for the scheduler
*
* @return a long.
*/
public long getCurrentTime() {
return System.currentTimeMillis();
}
/**
* 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.start();
m_status = STARTING;
LogUtils.debugf(this, "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();
m_runner.shutdown();
LogUtils.debugf(this, "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 m_runner.toString();
}
/**
* 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;
}
LogUtils.debugf(this, "run: scheduler running");
// Loop until a fatal exception occurs or until
// the thread is interrupted.
//
for (;;) {
// block if there is nothing in the queue(s)
// When something is added to the queue it
// signals us to wakeup
//
synchronized (this) {
if (m_status != RUNNING && m_status != PAUSED
&& m_status != PAUSE_PENDING
&& m_status != RESUME_PENDING) {
LogUtils.debugf(this, "run: status = %s, time to exit", m_status);
break;
}
if (m_scheduled == 0) {
try {
LogUtils.debugf(this, "run: no interfaces scheduled, waiting...");
wait();
} catch (InterruptedException ex) {
break;
}
}
}
// cycle through the queues checking for
// what's ready to run. The queues are keyed
// by the interval, but the mapped elements
// are peekable fifo queues.
//
int runned = 0;
synchronized (m_queues) {
// get an iterator so that we can cycle
// through the queue elements.
//
Iterator<Long> iter = m_queues.keySet().iterator();
while (iter.hasNext()) {
// Peak for Runnable objects until
// there are no more ready runnables
//
// Also, only go through each queue once!
// if we didn't add a count then it would
// be possible to starve other queues.
//
Long key = iter.next();
PeekableFifoQueue<ReadyRunnable> in = m_queues.get(key);
if (in.isEmpty()) {
continue;
}
ReadyRunnable readyRun = null;
int maxLoops = in.size();
do {
try {
readyRun = in.peek();
if (readyRun != null && readyRun.isReady()) {
LogUtils.debugf(this, "run: found ready runnable %s", readyRun.getInfo());
// Pop the interface/readyRunnable from the
// queue for execution.
//
in.remove();
// Add runnable to the execution queue
m_runner.execute(readyRun);
++runned;
}
} catch (InterruptedException ex) {
return; // jump all the way out
}
} while (readyRun != null && readyRun.isReady()
&& --maxLoops > 0);
}
}
// Wait for 1 second if there were no runnables
// executed during this loop, otherwise just
// start over.
//
synchronized (this) {
m_scheduled -= runned;
if (runned == 0) {
try {
wait(1000);
} catch (InterruptedException ex) {
break; // exit for loop
}
}
}
} // end for(;;)
LogUtils.debugf(this, "run: scheduler exiting, state = STOPPED");
synchronized (this) {
m_status = STOPPED;
}
} // end run
}