/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sun.jini.thread;
import com.sun.jini.config.Config;
import com.sun.jini.constants.TimeConstants;
import java.text.DateFormat;
import java.util.SortedSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.PrintWriter;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
/**
* A Queue of timed tasks. Each task implements {@link Runnable}.
* Events can either be executed in the queue's thread or in their own thread.
* <p>
* A task is an object that implements <code>Runnable</code>. It is
* scheduled by invoking {@link #schedule(long, Runnable, WakeupManager.ThreadDesc)
* schedule} with a time at which it should be run. When that time
* arrives (approximately) the task will be pulled off the queue and
* have its {@link Runnable#run run} method invoked. <p>
*
* A <code>schedule</code> request can specify a
* {@link WakeupManager.ThreadDesc}, which will define the parameters
* of a thread to be created to run the <code>Runnable</code>. You can
* specify the group, whether the thread is a daemon thread, and the priority.
* Additionally you can use a subclass of <code>WakeupManager.ThreadDesc</code>
* and override the {@link WakeupManager.ThreadDesc#thread thread} method
* to further customize thread creation.
* <p>
*
* When a task is scheduled, a {@link WakeupManager.Ticket} is returned
* that can be used to cancel the event if desired.
* <p>
*
* The queue requires its own thread, whose parameters can be defined
* via a <code>ThreadDesc</code> if desired. The queue's thread
* will be started when the first task is scheduled. If the queue
* becomes empty the thread will be terminated after a
* <a href=#queueThreadTimeout>configurable delay</a>. The thread
* will be re-started if a new task is scheduled.
* <p>
*
* While it is theoretically possible to obtain the queue's thread and
* interrupt it, the results of doing so are undefined. If a client
* wishes to stop the queue's thread the client should either remove
* all the tasks or call {@link #stop}. Note, calling
* <code>stop</code> will cause future <code>schedule</code> calls to
* fail with an <code>IllegalStateException</code>. <p>
*
* <a name="ConfigEntries">
* <code>WakeupManager</code> supports the <code>queueThreadTimeout</code>
* configuration entry, with the component
* <code>com.sun.jini.thread.WakeupManager</code>.
*
* <a name="queueThreadTimeout">
* <table summary="Describes the queueThreadTimeout configuration entry"
* border="0" cellpadding="2">
* <tr valign="top">
* <th scope="col" summary="layout"> <font size="+1">•</font>
* <th scope="col" align="left" colspan="2"> <font size="+1">
* <code>queueThreadTimeout</code></font>
*
* <tr valign="top"> <td>   <th scope="row" align="right">
* Type: <td> <code>long</code>
*
* <tr valign="top"> <td>   <th scope="row" align="right">
* Default: <td> 30,000 milliseconds
*
* <tr valign="top"> <td>   <th scope="row" align="right">
* Description:
* <td> How long, in milliseconds, the queue's thread will be
* left running if there are no scheduled tasks. Must be
* a non-negative long value. This configuration entry is
* consulted when the <code>WakeupManager</code> is initially created.
*
* </table>
* <p>
*
* This class uses the {@link Logger} named
* <code>com.sun.jini.thread.WakeupManager</code> to log information at
* the following logging levels: <p>
*
* <table border=1 cellpadding=5
* summary="Describes logging performed by WakeupManager at different
* logging levels">
*
* <tr> <th> Level <th> Description
*
* <tr> <td> SEVERE <td> exceptions thrown when we attempt to
* create the queue's thread
*
* <tr> <td> WARNING <td> exceptions thrown by the run methods of tasks,
* by the <code>ThreadDesc</code>'s of tasks, or
* if the queue's thread is interrupted
*
* <tr> <td> FINEST <td> how many milliseconds until the next event
* and when the queue's thread is stopped or started
*
* </table>
*
* @author Sun Microsystems, Inc.
*
* @see java.lang.Runnable */
public class WakeupManager {
/** Component we pull configuration entries from and our logger name */
private final static String COMPONENT_NAME =
"com.sun.jini.thread.WakeupManager";
/** Default value for <code>queueThreadTimeout</code> */
private final static long DEFAULT_QUEUE_THREAD_TIMEOUT = 30000;
/**
* If there are no registered tasks number of
* milliseconds to wait before killing the kicker thread
*/
private final long queueThreadTimeout;
/**
* The queue. Also the object we use for locking, multi-threaded
* access to all the other fields is arbitrated by synchronizing
* on this object.
*/
private final SortedSet contents = new java.util.TreeSet();
/** <code>ThreadDesc</code> we use to create kicker threads */
private final ThreadDesc kickerDesc;
/** The Runnable for the queue's thread */
private final Kicker kicker = new Kicker();
/** Next tie breaker ticket */
private long nextBreaker = 0;
/** First item in contents */
private Ticket head = null;
/** The queue's thread */
private Thread kickerThread;
/**
* <code>true</code> if we have been stopped.
*/
private boolean dead = false;
/**
* <code>DataFormat</code> used by {@link Ticket} to format its
* <code>toString</code> return value.
*/
private static DateFormat dateFmt =
DateFormat.getTimeInstance(DateFormat.LONG);
/** Logger for this class and nested classes */
private static final Logger logger = Logger.getLogger(COMPONENT_NAME);
/**
* Description of a future thread.
*
* @see WakeupManager#schedule
* @see WakeupManager#WakeupManager(WakeupManager.ThreadDesc)
*/
public static class ThreadDesc {
private final ThreadGroup group; // group to create in
private final boolean daemon; // create as daemon?
private final int priority; // priority
/**
* Equivalent to
* <pre>
* ThreadDesc(null, false)
* </pre>
*/
public ThreadDesc() {
this(null, false);
}
/**
* Equivalent to
* <pre>
* ThreadDesc(group, deamon, Thread.NORM_PRIORITY)
* </pre>
*/
public ThreadDesc(ThreadGroup group, boolean daemon) {
this(group, daemon, Thread.NORM_PRIORITY);
}
/**
* Describe a future thread that will be created in the given group,
* deamon status, and priority.
*
* @param group The group to be created in. If <code>null</code>,
* the thread will be created in the default group.
* @param daemon The thread will be a daemon thread if this is
* <code>true</code>.
* @param priority The thread's priority.
* @throws IllegalArgumentException if priority is not
* in between {@link Thread#MIN_PRIORITY} and
* {@link Thread#MAX_PRIORITY}
*/
public ThreadDesc(ThreadGroup group, boolean daemon, int priority) {
if (priority < Thread.MIN_PRIORITY ||
priority > Thread.MAX_PRIORITY)
{
throw new IllegalArgumentException("bad value for priority:" +
priority);
}
this.group = group;
this.daemon = daemon;
this.priority = priority;
}
/**
* The {@link ThreadGroup} the thread will be created in.
* @return the {@link ThreadGroup} the thread will be created in.
*/
public ThreadGroup getGroup() { return group; }
/**
* Returns <code>true</code> if the the thread will be daemon
* thread, returns <code>false</code> otherwise.
* @return <code>true</code> if the the thread will be daemon
* thread, returns <code>false</code> otherwise.
*/
public boolean isDaemon() { return daemon; }
/**
* The priority the thread should be created with.
* @return the priority the thread should be created with.
*/
public int getPriority() { return priority; }
/**
* Create a thread for the given runnable based on the values in this
* object. May be overridden to give full control over creation
* of thread.
* @return a thread to run <code>r</code>, unstarted
*/
public Thread thread(Runnable r) {
Thread thr;
if (getGroup() == null)
thr = new Thread(r);
else
thr = new Thread(getGroup(), r);
thr.setDaemon(isDaemon());
thr.setPriority(getPriority());
return thr;
}
public String toString() {
return "[" + getGroup() + ", " + isDaemon() + ", "
+ getPriority() + "]";
}
}
/**
* A ticket that can be used for cancelling a future task. It
* describes the task itself as well. The {@link
* WakeupManager#newTicket WakeupManager.newTicket} method
* can be used by subclasses of <code>WakeupManager</code> to
* create new <code>Ticket</code> instances.
*/
public static class Ticket implements Comparable {
/** When the task should occur. */
public final long when;
/** The task object to be executed */
public final Runnable task;
/** The <code>ThreadDesc</code>, or <code>null</code> if none. */
public final ThreadDesc desc;
/** Tie beaker used when two tickets have the same value for when */
private final long breaker;
private Ticket(long when, Runnable task, ThreadDesc threadDesc,
long breaker)
{
if (task == null)
throw new NullPointerException("task not specified");
this.when = when;
this.task = task;
this.desc = threadDesc;
this.breaker = breaker;
}
public String toString() {
return dateFmt.format(new Long(when)) + "(" + when + ")" + ", "
+ task.getClass().getName() + ", " + desc;
}
public boolean equals(Object o) {
if (!(o instanceof Ticket))
return false;
final Ticket that = (Ticket)o;
return that.when == when && that.breaker == breaker;
}
public int hashCode() {
return (int)breaker;
}
public int compareTo(Object o) {
final Ticket that = (Ticket)o;
final long whenDiff = when - that.when;
if (whenDiff > 0)
return 1;
else if (whenDiff < 0)
return -1;
else {
final long breakerDiff = breaker - that.breaker;
if (breakerDiff > 0)
return 1;
else if (breakerDiff < 0)
return -1;
else
return 0;
}
}
}
/**
* Create a new <code>WakeupManager</code>. Equivalent to.
* <pre>
* WakeupManager(new ThreadDesc())
* </pre>
*
* @see WakeupManager.ThreadDesc
*/
public WakeupManager() {
this(new ThreadDesc());
}
/**
* Create a new <code>WakeupManager</code>. The thread used for
* timing will be created according to the provided <code>ThreadDesc</code>.
* @throws NullPointerException if desc is null
*/
public WakeupManager(ThreadDesc desc) {
if (desc == null)
throw new NullPointerException("desc must be non-null");
kickerDesc = desc;
queueThreadTimeout = DEFAULT_QUEUE_THREAD_TIMEOUT;
}
/**
* Create a new <code>WakeupManager</code>. The thread used for
* timing will be created according to the provided <code>ThreadDesc</code>.
* Optionally pass a configuration to control various implementation
* specific behaviors.
* @throws ConfigurationException if if an exception
* occurs while retrieving an item from the given
* <code>Configuration</code> object
* @throws NullPointerException if either argument is null
*/
public WakeupManager(ThreadDesc desc, Configuration config)
throws ConfigurationException
{
if (desc == null)
throw new NullPointerException("desc must be non-null");
kickerDesc = desc;
queueThreadTimeout = Config.getLongEntry(config, COMPONENT_NAME,
"queueThreadTimeout", DEFAULT_QUEUE_THREAD_TIMEOUT,
0, Long.MAX_VALUE);
}
/**
* Create a new ticket with the specified values for when the task
* should be run, what task should be run, and what sort of
* thread the task should be run in.
*
* @param when when the task should run, an absolute time
* @param task what task should be run
* @param threadDesc if non-<code>null</code> the object to use to
* create the thread the task should be run in, if
* <code>null</code> the task should be run in the
* manager's thread.
* @throws NullPointerException if task is <code>null</code>
*/
protected Ticket newTicket(long when, Runnable task, ThreadDesc threadDesc) {
synchronized (contents) {
return new Ticket(when, task, threadDesc, nextBreaker++);
}
}
/**
* Schedule the given task for the given time. The task's <code>run</code>
* method will be executed synchronously in the queue's own thread, so it
* should be brief or it will affect whether future events will be executed
* at an appropriate time.
* @throws NullPointerException if <code>task</code> is <code>null</code>
* @throws IllegalStateException if the manager has been stopped
*/
public Ticket schedule(long when, Runnable task) {
return schedule(when, task, null);
}
/**
* Schedule the given task for the given time, to be run in a thread.
* When the time comes, a new thread will be created according to the
* <code>ThreadDesc</code> object provided. If <code>threadDesc</code> is
* <code>null</code>, this is equivalent to the other form of
* <code>schedule</code>.
* @throws NullPointerException if <code>task</code> is <code>null</code>
* @throws IllegalStateException if the manager has been stopped
*/
public Ticket schedule(long when, Runnable task, ThreadDesc threadDesc) {
synchronized (contents) {
if (dead)
throw new IllegalStateException(
"trying to add task to stopped WakeupManager");
Ticket t = newTicket(when, task, threadDesc);
contents.add(t);
if (kickerThread == null) {
logger.log(Level.FINEST, "starting queue's thread");
try {
final Thread thread = kickerDesc.thread(kicker);
thread.start();
// Only set once we know start worked
kickerThread = thread;
} catch (Throwable tt) {
try {
logger.log(Level.SEVERE,
"queue thread creation exception",tt);
} catch (Throwable ttt) {
// don't let a problem in logging kill the thread
}
}
}
// need to call checkHead (even if we just (re)created the
// kickerThread), because that is how head gets set (note,
// this is ok to call even if thread creation failed)
checkHead();
return t;
}
}
/**
* Cancel the given ticket.
*/
public void cancel(Ticket t) {
synchronized (contents) {
if (dead) return;
contents.remove(t);
checkHead();
}
}
/**
* Cancel all tickets.
*/
public void cancelAll() {
synchronized (contents) {
if (dead) return;
contents.clear();
checkHead();
}
}
/**
* Called whenever we change contents to update head
* and see if we need to wake up the queue thread.
* Assumes the caller holds the lock on contents.
*/
private void checkHead() {
assert Thread.holdsLock(contents);
final Ticket oldHead = head;
if (contents.isEmpty())
head = null;
else
head = (Ticket)contents.first();
if (head == oldHead) return;
// New first event (including possibly no events), run
// needs to wake up and change its sleep time.
contents.notifyAll();
}
/**
* Return whether the queue is currently empty.
*/
public boolean isEmpty() {
synchronized (contents) {
return (contents.isEmpty());
}
}
/**
* Stop executing.
*/
public void stop() {
synchronized (contents) {
contents.clear();
kickerThread = null;
head = null;
dead = true;
contents.notifyAll();
}
}
/**
* The kicker work. This is what sleeps until the time of
* the next event.
*/
private class Kicker implements Runnable {
public void run() {
/* Set when contents is empty to control when the kicker will
* exit. Long.MIN_VALUE used as flag value to indicate
* kickerExitTime is invalid
*/
long kickerExitTime = Long.MIN_VALUE;
while (true) {
final Ticket ticketToRun;
synchronized (contents) {
while (true) {
if (dead)
return;
final long now = System.currentTimeMillis();
final long timeToNextEvent;
if (contents.isEmpty()) {
if (kickerExitTime == Long.MIN_VALUE) {
kickerExitTime = now + queueThreadTimeout;
if (kickerExitTime < 0) {
// overflow
kickerExitTime = Long.MAX_VALUE;
}
}
// Since contents is empty the next event is exit
timeToNextEvent = kickerExitTime - now;
if (timeToNextEvent <= 0) {
// been idle long enough, depart
/* $$$ Do this in a finally block for the run?
* so no mater how this thread ends kickerThread
* get set to null?
*/
kickerThread = null;
logger.log(Level.FINEST,
"stopping queue's thread");
return;
}
} else { // contents is non-empty
kickerExitTime = Long.MIN_VALUE;
timeToNextEvent = head.when - now;
if (timeToNextEvent <= 0) {
// The head's time has come, consume and
// break out of inner loop to run it.
ticketToRun = head;
contents.remove(head);
checkHead();
break;
}
}
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "timeToNextEvent:{0}",
(timeToNextEvent == Long.MAX_VALUE ?
"Long.MAX_VALUE" :
Long.toString(timeToNextEvent)));
}
assert timeToNextEvent > 0;
try {
contents.wait(timeToNextEvent);
} catch (InterruptedException e) {
/* This should never happen, our thread is
* private to WakeupManager and tasks
* calling Thread.currentThread().interrupt() is
* decidedly anti-social. Log, but keep on
* going.
*/
try {
logger.log(Level.WARNING,
"Attempt to interrupt Queue's thread");
} catch (Throwable t) {
// ignore
}
/* This loop already deals with wait returning
* early for no reason, so going to the top
* of the loop is ok here - if there are no
* new tasks and we are not dead we will
* just calc a new value for timeToNextEvent
*/
}
/* Something has changed or the time has arived
* for action, don't know which, go back to the
* the top of the inner loop to figure out what to
* do next
*/
}
}
// Run the task outside of the lock
if (ticketToRun.desc == null) {
// ... in this thread
try {
ticketToRun.task.run();
} catch (Throwable e) {
try {
logger.log(Level.WARNING, "Runnable.run exception", e);
} catch (Throwable t) {
// don't let a problem in logging kill the thread
}
}
} else {
// ... in its own thread
try {
ticketToRun.desc.thread(ticketToRun.task).start();
} catch (Throwable t) {
try {
logger.log(Level.WARNING,
"task thread creation exception", t);
} catch (Throwable tt) {
// don't let a problem in logging kill the thread
}
}
}
}
}
}
}