/* scheduleHandle.java This class is used to keep track of background tasks running on the Ganymede Server. It is also used to pass data to the admin console. Created: 3 February 1998 Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2014 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program 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 2 of the License, or (at your option) any later version. This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.ganymede.common; import java.util.Date; import arlut.csd.Util.booleanSemaphore; import arlut.csd.Util.TranslationService; import arlut.csd.ganymede.server.Ganymede; import arlut.csd.ganymede.server.GanymedeBuilderTask; import arlut.csd.ganymede.server.GanymedeScheduler; import arlut.csd.ganymede.server.taskMonitor; /*------------------------------------------------------------------------------ class scheduleHandle ------------------------------------------------------------------------------*/ /** * <p>Handle object used to help manage background tasks registered in * the Ganymede Server's {@link * arlut.csd.ganymede.server.GanymedeScheduler GanymedeScheduler}. In * addition to being used by the server's task scheduler to organize * and track registered tasks, vectors of serialized scheduleHandle * objects are passed to the Ganymede admin console's {@link * arlut.csd.ganymede.admin.GASHAdminDispatch#changeTasks(java.lang.Object[]) * changeTasks} method.</p> * * <p>Within the Ganymede server, scheduleHandle objects are held * within the GanymedeScheduler to track the status of each registered * task. When the GanymedeScheduler needs to run a background task, * the scheduleHandle's {@link * arlut.csd.ganymede.common.scheduleHandle#runTask() runTask()} * method is called. runTask() creates a pair of threads, one to run * the task and another {@link arlut.csd.ganymede.server.taskMonitor * taskMonitor} thread to wait for the task to be completed. When the * thread running the task completes, the task's taskMonitor calls the * scheduleHandle's {@link * arlut.csd.ganymede.common.scheduleHandle#notifyCompletion * notifyCompletion()} method, which in turn notifies the * GanymedeScheduler that the task has completed its execution.</p> * * <p>The various scheduling methods in scheduleHandle will throw an * IllegalArgumentException if called post-serialization on the * Ganymede client.</p> * * <p>In order to avoid nested monitor deadlock, all calls from * scheduleHandle to synchronized methods on the GanymedeScheduler * must be made from locally unsynchronized code blocks.</p> */ public class scheduleHandle implements java.io.Serializable { static final long serialVersionUID = 4631286257914310550L; static final boolean debug = false; /** * TranslationService object for handling string localization in the * Ganymede system. */ static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.common.scheduleHandle"); // --- static public enum TaskType { SCHEDULED (ts.l("taskType.scheduledTask")), // "Scheduled Task" MANUAL (ts.l("taskType.manualTask")), // "On Demand Task" BUILDER (ts.l("taskType.builderTask")), // "Builder Task" UNSCHEDULEDBUILDER (ts.l("taskType.unscheduledBuilderTask")), // "Unscheduled Ganymede Builder Task" SYNCINCREMENTAL (ts.l("taskType.incrementalSync")), // "Incremental" SYNCFULLSTATE (ts.l("taskType.fullstateSync")), // "Full State" SYNCMANUAL (ts.l("taskType.manualSync")); // "Manual" private final String name; TaskType(String name) { this.name = name; } public String toString() { return this.name; } } static public enum TaskStatus { /** * <p>OK used for scheduled, manual, builder, unscheduled builder, * syncfullstate, syncmanual TaskTypes.</p> * * <p>Indicates an uneventful condition, with everything as it * should be.</p> */ OK() { @Override public String getMessage(int queueSize, String condition) { // " " return ts.l("taskStatus.ok"); } }, /** * <p>EMPTYQUEUE only used for SYNCINCREMENTAL. Has the * connotation of OK, but specifically indicates that the queue is * empty for an incremental sync channel.</p> */ EMPTYQUEUE() { @Override public String getMessage(int queueSize, String condition) { // "Good, Queue is empty" return ts.l("taskStatus.emptyQueue"); } }, /** * <p>NONEMPTYQUEUE only used for SYNCINCREMENTAL. Has the * connotation of OK, but specifically indicates that the queue is * not empty for an incremental sync channel.</p> */ NONEMPTYQUEUE() { @Override public String getMessage(int queueSize, String condition) { // "Good, Queue size is {0,number,#}" return ts.l("taskStatus.nonEmptyQueue", queueSize); } }, /** * <p>STUCKQUEUE only used for SYNCINCREMENTAL. Has the * connotation of SERVICEERROR, SERVICEFAIL, or FAIL, but * specifically indicates that the queue is not being successfully * serviced, meaning that the lowest numbered transaction in the * queue remained in the queue after the service was last run.</p> */ STUCKQUEUE() { @Override public String getMessage(int queueSize, String condition) { // "Stuck, Queue size is {0,number,#}. {1}" return ts.l("taskStatus.stuckQueue", queueSize, condition); } }, /** * <p>SERVICEERROR is used for BUILDER, UNSCHEDULEDBUILDER, * SYNCFULLSTATE, SYNCMANUAL, and indicates that the service * program associated with this task could not be successfully * started, due to a missing service program or a permissions * error.</p> */ SERVICEERROR() { @Override public String getMessage(int queueSize, String condition) { // "Service program could not be run: {0}" return ts.l("taskStatus.serviceError", condition); } }, /** * <p>SERVICEFAIL is used for BUILDER, UNSCHEDULEDBUILDER, * SYNCFULLSTATE, SYNCMANUAL, and indicates that the service * program associated with this task was able to be started, but * exited with a failure code.</p> */ SERVICEFAIL() { @Override public String getMessage(int queueSize, String condition) { // "Service program failure: {0}" return ts.l("taskStatus.serviceFail", condition); } }, /** * <p>FAIL can be used for any task type, and indicates some kind * of unexpected exception was thrown.</p> */ FAIL() { @Override public String getMessage(int queueSize, String condition) { // "Error: {0}" return ts.l("taskStatus.fail", condition); } }; TaskStatus() { } public String getMessage() { return getMessage(0, ""); } public String getMessage(int queueSize) { return getMessage(queueSize, ""); } public String getMessage(String condition) { return getMessage(0, condition); } abstract public String getMessage(int queueSize, String condition); } // we pass these attributes along to the admin console for it to display /** * What type of TaskStatus object corresponds to this * scheduleHandle. */ public TaskStatus status; /** * What kind of task is this? Used to provide type information to * the Ganymede admin console. */ public TaskType tasktype; /** * When was this task last issued? */ public Date lastTime; /** * When will this task next be issued? */ public Date startTime; /** * when was this task first registered? used to present a * consistently sorted list on the client */ public Date incepDate; /** * For reporting our interval status to the admin console */ public String intervalString; /** * For reporting our name to the admin console */ public String name; /** * For reporting our queue size (for incremental sync channels) to * the admin console */ public int queueSize; /** * For reporting the details of a fault condition of some kind to * the admin console */ public String condition; /** * <p>This booleanSemaphore will be set to true if the task * associated with this scheduleHandle is currently running.</p> * * <p>It is a booleanSemaphore so that we can have an appropriate * memory barrier for multiprocessor access without having to * synchronize on the scheduleHandle upon calls to isRunning().</p> */ private booleanSemaphore isRunning = new booleanSemaphore(false); /** * <p>This booleanSemaphore will be set to true if the task * associated with this scheduleHandle is currently suspended.</p> * * <p>It is a booleanSemaphore so that we can have an appropriate * memory barrier for multiprocessor access without having to * synchronize on the scheduleHandle upon calls to * isSuspended().</p> */ private booleanSemaphore suspend = new booleanSemaphore(false); /** * <p>This booleanSemaphore will be set to true if we are doing a * on-demand and we get a request while running it, which signifies * that we'll want to immediately re-run it on completion.</p> * * <p>It is a booleanSemaphore so that we can have an appropriate * memory barrier for multiprocessor access without having to * synchronize on the scheduleHandle upon calls to runAgain().</p> */ private booleanSemaphore rerun = new booleanSemaphore(false); /** * <p>The time on the server that this task was started running.</p> */ private Date runStartTime; /** * <p>The most recent time seen on the server before this * scheduleHandle is transmitted to the admin console. Used to * calculate the age of this task's runtime without respect to time * differences between the server and the machine running the admin * console.</p> */ private Date recentServerTime; // // non-serializable, for use on the server only // /** * Any options that we need to pass to the task? */ transient public Object options[]; /** * 0 if this is a one-shot, otherwise, the count in minutes */ transient public int interval; /** * The task to run */ transient public Runnable task; /** * If this task is currently running, this field will * point to the running thread, otherwise it will be null. */ transient public Thread thread; /** * If this task is currently running, this field will * point to a monitor thread that is waiting for the task's * thread to complete before reporting completion. The * monitor thread is effectively a wrapper thread that * lets any arbitrary Runnable be scheduled and managed * by GanymedeScheduler. */ transient public Thread monitor; /** * The GanymedeScheduler that this task is registered in. */ transient public GanymedeScheduler scheduler = null; /* -- */ /** * @param scheduler Reference to the server's GanymedeScheduler * @param time Anchor point for interval calculations * @param interval Number of seconds between runs of this task * @param task Java Runnable object to be run in a thread * @param name Name to report for this task * @param tasktype Task type category for this handle */ public scheduleHandle(GanymedeScheduler scheduler, Date time, int interval, Runnable task, String name, TaskType tasktype) { if (scheduler == null) { throw new IllegalArgumentException("can't create schedule handle without scheduler reference"); } this.scheduler = scheduler; this.lastTime = null; this.startTime = time != null ? new Date(time.getTime()) : null; this.task = task; this.name = name; this.tasktype = tasktype; this.status = TaskStatus.OK; this.runStartTime = null; setInterval(interval); // remember when we were created incepDate = new Date(); } /** * <p>This method is used to set an options array for the next run * of the task associated with this handle, if that task is a * GanymedeBuilderTask.</p> * * <p>If the task associated with this handle is not a * GanymedeBuilderTask, the options will be ignored. Since * setOptions() is synchronized, options may only be set at a time * when runTask() is not busy issuing the task in the background. * runTask() clears the options set, so setOptions() only affects * the next launching of the task.</p> */ public void setOptions(Object _options[]) { this.options = _options; } /** * <p>Runs this task in a background thread. A second background thread * is created to handle a {@link arlut.csd.ganymede.server.taskMonitor taskMonitor} * to wait and report when the task completes.</p> * * <p>This method is invalid on the client.</p> */ public void runTask() { // start our task if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } if (debug) { System.err.println("Ganymede Scheduler: Starting task " + name + " at " + new Date()); } try { synchronized (this) { rerun.set(false); if (suspend.isSet()) { Ganymede.debug("Ganymede Scheduler: Task " + name + " skipped at " + new Date()); throw new suspendedTaskException(); // goto a-go-go } this.runStartTime = new Date(); // grab options for this run if (options == null || (!(task instanceof GanymedeBuilderTask))) { thread = new Thread(task, name); thread.start(); } else { // we're running a GanymedeBuilderTask with options set final Object _options[] = options; final GanymedeBuilderTask _task = (GanymedeBuilderTask) task; thread = new Thread(new Runnable() { public void run() { _task.run(_options); } }, name); thread.start(); // clear options this.options = null; } isRunning.set(true); // and have our monitor watch for it monitor = new Thread(new taskMonitor(thread, this), name); monitor.start(); } } catch (suspendedTaskException ex) { // XXX must not be locally synchronized here, else possible // nested monitor deadlock scheduler.notifyCompletion(this); return; } } /** * <p>This method is called by our {@link * arlut.csd.ganymede.server.taskMonitor taskMonitor} when our task * completes. This method has no meaning outside of the context of * the taskMonitor spawned by this handle, and should not be called * from any other code.</p> */ public void notifyCompletion() { synchronized (this) { if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } monitor = null; thread = null; runStartTime = null; isRunning.set(false); lastTime = new Date(); } // XXX must not be synchronized on this scheduleHandle here, // else possible nested monitor deadlock scheduler.notifyCompletion(this); } /** * <p>Server-side method to determine whether this task should be * rescheduled. Increments the startTime and returns true if this * is a periodically executed task.</p> */ public synchronized boolean reschedule() { if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } if (startTime == null || interval == 0) { return false; } else { startTime.setTime(startTime.getTime() + (60000L * interval)); return true; } } /** * <p>Returns an enum value indicating what type of Ganymede task * this scheduleHandle is pertaining to.</p> */ public TaskType getTaskType() { return this.tasktype; } /** * <p>Returns an expanded status for the task associated with this * handle.</p> */ public TaskStatus getTaskStatus() { return this.status; } /** * <p>Updates the task status for this scheduleHandle.</p> */ public void setTaskStatus(TaskStatus newStatus, int queueSize, String condition) { this.status = newStatus; this.queueSize = queueSize; this.condition = condition; } /** * <p>Returns the name of this handle.</p> */ public String getName() { return this.name; } /** * <p>This method lets the GanymedeScheduler check to see whether * this task should be re-run when it terminates</p> */ public boolean runAgain() { return rerun.isSet(); } /** * <p>Returns true if this task is not scheduled for periodic * execution</p> */ public boolean isOnDemand() { if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } return interval == 0; } /** * <p>Returns true if the task is currently running.</p> */ public boolean isRunning() { return isRunning.isSet(); } /** * <p>Returns true if the task is suspended.</p> */ public boolean isSuspended() { return suspend.isSet(); } /** * <p>Load the current server time into this scheduleHandle.</p> * * <p>We do this so the admin console which is about to receive a * serialized copy of this scheduleHandle has a reference with which * to calculate the duration of this task on the server, without * worrying about any clock skew between the server and console.</p> */ public synchronized void updateServerTime() { if (runStartTime != null) { recentServerTime = new Date(); } else { recentServerTime = null; } } /** * <p>Returns the length of time this task has been running, in * seconds, or -1 if the task is not running.</p> */ public synchronized int getAge() { if (runStartTime == null || recentServerTime == null) { return -1; } long currentTime = recentServerTime.getTime(); long myStartTime = runStartTime.getTime(); return (int) (currentTime - myStartTime) / 1000; } /** * <p>Server-side method to request that this task be re-run after * its current completion. Intended for on-demand tasks that are * requested by the {@link * arlut.csd.ganymede.server.GanymedeScheduler GanymedeScheduler} * while they are already running.</p> */ public void runOnCompletion() { this.runOnCompletion(null); } /** * <p>Server-side method to request that this task be re-run after * its current completion. Intended for on-demand tasks that are * requested by the {@link * arlut.csd.ganymede.server.GanymedeScheduler GanymedeScheduler} * while they are already running.</p> */ public synchronized void runOnCompletion(Object _options[]) { if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } this.options = _options; rerun.set(true); } /** * <p>Server-side method to request that this task not be kept after * its current completion. Used to remove a task from the Ganymede * scheduler.</p> */ public synchronized void unregister() { if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } rerun.set(false); } /** * <p>Server-side method to interrupt this task. Tasks must be * specifically written to properly respond to an interruption.</p> */ public synchronized void stop() { if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } if (thread != null) { thread.interrupt(); // this used to be a stop, but stop is deprecated // as unsafe in 1.2, so we do the next best thing } } /** * <p>Server-side method to disable future invocations of this * task</p> */ public synchronized void disable() { if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } suspend.set(true); } /** * <p>Server-side method to enable future invocations of this * task</p> */ public synchronized void enable() { if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } suspend.set(false); } /** * <p>Server-side method to change the interval for this task</p> * * @param interval Number of seconds between runs of this task */ public synchronized void setInterval(int interval) { if (scheduler == null) { throw new IllegalArgumentException("can't run this method on the client"); } // set the interval time this.interval = interval; if (interval == 0) { intervalString = ""; return; } // ok, we need to calculate a description for how long // between invocations of this task StringBuilder buff = new StringBuilder(); int minutes = 0; int hours = 0; int days = 0; int weeks = 0; int temp = interval; weeks = temp / 10080; temp %= 10080; days = temp / 1440; temp %= 1440; hours = temp / 60; temp %= 60; minutes = temp; if (weeks != 0) { if (weeks > 1) { // "{0,num,#} weeks" buff.append(ts.l("setInterval.weeks_pattern", Integer.valueOf(weeks))); } else { // "1 week" buff.append(ts.l("setInterval.week_pattern")); } } if (days != 0) { if (buff.length() != 0) { buff.append(", "); } if (days > 1) { // "{0,num,#} days" buff.append(ts.l("setInterval.days_pattern", Integer.valueOf(days))); } else { // "1 day" buff.append(ts.l("setInterval.day_pattern")); } } if (hours != 0) { if (buff.length() != 0) { buff.append(", "); } if (hours > 1) { // "{0,num,#} hours" buff.append(ts.l("setInterval.hours_pattern", Integer.valueOf(hours))); } else { // "1 hour" buff.append(ts.l("setInterval.hour_pattern")); } } if (minutes != 0) { if (buff.length() != 0) { buff.append(", "); } if (minutes > 1) { // "{0,num,#} minutes" buff.append(ts.l("setInterval.minutes_pattern", Integer.valueOf(minutes))); } else { // "1 minute" buff.append(ts.l("setInterval.minute_pattern")); } } // and set the string intervalString = buff.toString(); } } /*------------------------------------------------------------------------------ class suspendedTaskException ------------------------------------------------------------------------------*/ /** * Locally defined exception, used to throw control out of a * synchronized block in scheduleHandle.runTask if the task is * suspended. */ class suspendedTaskException extends RuntimeException { public suspendedTaskException() { super(); } public suspendedTaskException(String s) { super(s); } }