All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hsqldb.lib.HsqlTimer Maven / Gradle / Ivy

There is a newer version: 2.7.2
Show newest version
/* Copyright (c) 2001-2019, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.lib;

import java.util.Comparator;
import java.util.Date;

/**
 * Facility to schedule tasks for future execution in a background thread.

* * Tasks may be scheduled for one-time execution or for repeated execution at * regular intervals, using either fixed rate or fixed delay policy.

* * This class is a JDK 1.1 compatible implementation required by HSQLDB both * because the java.util.Timer class is available only in JDK 1.3+ and because * java.util.Timer starves least recently added tasks under high load and * fixed rate scheduling, especially when the average actual task duration is * greater than the average requested task periodicity.

* * An additional (minor) advantage over java.util.Timer is that this class does * not retain a live background thread during periods when the task queue is * empty. * @author Campbell Burnet (campbell-burnet@users dot sourceforge.net) * @version 1.9.0 * @since 1.7.2 */ public final class HsqlTimer implements Comparator, ThreadFactory { /** The priority queue for the scheduled tasks. */ protected final TaskQueue taskQueue = new TaskQueue(16, this); /** The inner runnable that executes tasks in the background thread. */ protected final TaskRunner taskRunner = new TaskRunner(); /** The background thread. */ protected Thread taskRunnerThread; /** The factory that produces the background threads. */ protected final ThreadFactory threadFactory; /** * Whether this timer should disallow all further processing. * * Once set true, stays true forever. */ protected volatile boolean isShutdown; /** * Constructs a new HsqlTimer using the default thread factory * implementation. */ public HsqlTimer() { this(null); } /** * Constructs a new HsqlTimer. * * Uses the specified thread factory implementation. * * @param threadFactory the ThreadFactory used to produce this timer's * background threads. If null, the default implementation supplied * by this class will be used. */ public HsqlTimer(final ThreadFactory threadFactory) { this.threadFactory = (threadFactory == null) ? this : threadFactory; } /** * Required to back the priority queue for scheduled tasks. * * @param a the first Task * @param b the second Task * @return 0 if equal, < 0 if a < b, > 0 if a > b */ public int compare(final Object a, final Object b) { final long awhen = ((Task) a).getNextScheduled(); final long bwhen = ((Task) b).getNextScheduled(); return (awhen < bwhen) ? -1 : (awhen == bwhen) ? 0 : 1; } /** * Default ThreadFactory implementation.

* * Constructs a new Thread from the designated runnable, sets its * name to "HSQLDB Timer @" + Integer.toHexString(hashCode()), * and sets it as a daemon thread.

* * @param runnable used to construct the new Thread. * @return a new Thread constructed from the designated runnable. */ public Thread newThread(final Runnable runnable) { final Thread thread = new Thread(runnable); thread.setName("HSQLDB Timer @" + Integer.toHexString(hashCode())); thread.setDaemon(true); return thread; } /** * Retrieves the background execution thread.

* * null is returned if there is no such thread.

* * @return the current background thread (may be null) */ public synchronized Thread getThread() { return this.taskRunnerThread; } /** * (Re)starts background processing of the task queue. * * @throws IllegalStateException if this timer is shut down. * @see #shutdown() * @see #shutdownImmediately() */ public synchronized void restart() throws IllegalStateException { if (this.isShutdown) { throw new IllegalStateException("isShutdown==true"); } else if (this.taskRunnerThread == null) { this.taskRunnerThread = this.threadFactory.newThread(this.taskRunner); this.taskRunnerThread.start(); } else { this.taskQueue.unpark(); } } /** * Causes the specified Runnable to be executed once in the background * after the specified delay. * * @param delay in milliseconds * @param runnable the Runnable to execute. * @return opaque reference to the internal task * @throws IllegalArgumentException if runnable is null */ public Object scheduleAfter(final long delay, final Runnable runnable) throws IllegalArgumentException { if (runnable == null) { throw new IllegalArgumentException("runnable == null"); } return this.addTask(now() + delay, runnable, 0, false); } /** * Causes the specified Runnable to be executed once in the background * at the specified time. * * @param date time at which to execute the specified Runnable * @param runnable the Runnable to execute. * @return opaque reference to the internal task * @throws IllegalArgumentException if date or runnable is null */ public Object scheduleAt(final Date date, final Runnable runnable) throws IllegalArgumentException { if (date == null) { throw new IllegalArgumentException("date == null"); } else if (runnable == null) { throw new IllegalArgumentException("runnable == null"); } return this.addTask(date.getTime(), runnable, 0, false); } /** * Causes the specified Runnable to be executed periodically in the * background, starting at the specified time. * * @return opaque reference to the internal task * @param period the cycle period * @param relative if true, fixed rate scheduling else fixed delay scheduling * @param date time at which to execute the specified Runnable * @param runnable the Runnable to execute * @throws IllegalArgumentException if date or runnable is null, or * period is <= 0 */ public Object schedulePeriodicallyAt(final Date date, final long period, final Runnable runnable, final boolean relative) throws IllegalArgumentException { if (date == null) { throw new IllegalArgumentException("date == null"); } else if (period <= 0) { throw new IllegalArgumentException("period <= 0"); } else if (runnable == null) { throw new IllegalArgumentException("runnable == null"); } return addTask(date.getTime(), runnable, period, relative); } /** * Causes the specified Runnable to be executed periodically in the * background, starting after the specified delay. * * @return opaque reference to the internal task * @param period the cycle period * @param relative if true, fixed rate scheduling else fixed delay scheduling * @param delay in milliseconds * @param runnable the Runnable to execute. * @throws IllegalArgumentException if runnable is null or period is <= 0 */ public Object schedulePeriodicallyAfter(final long delay, final long period, final Runnable runnable, final boolean relative) throws IllegalArgumentException { if (period <= 0) { throw new IllegalArgumentException("period <= 0"); } else if (runnable == null) { throw new IllegalArgumentException("runnable == null"); } return addTask(now() + delay, runnable, period, relative); } /** * Shuts down this timer after the current task (if any) completes.

* * After this call, the timer has permanently entered the shutdown state; * attempting to schedule any new task or directly restart this timer will * result in an IllegalStateException.

* */ public synchronized void shutdown() { if (!this.isShutdown) { this.isShutdown = true; this.taskQueue.cancelAllTasks(); } } /** for compatibility with previous version */ public synchronized void shutDown() { shutdown(); } /** * Shuts down this timer immediately, interrupting the wait state associated * with the current head of the task queue or the wait state internal to * the currently executing task, if any such state is currently in effect. * * After this call, the timer has permanently entered the shutdown state; * attempting to schedule any new task or directly restart this timer will * result in an IllegalStateException.

* * Note: If the integrity of work performed by a scheduled task * may be adversely affected by an unplanned interruption, it is the * responsibility of the task's implementation to deal correctly with the * possibility that this method is called while such work is in progress, * for instance by catching the InterruptedException, completing the work, * and then rethrowing the exception. */ public synchronized void shutdownImmediately() { if (!this.isShutdown) { final Thread runner = this.taskRunnerThread; this.isShutdown = true; if (runner != null && runner.isAlive()) { runner.interrupt(); } this.taskQueue.cancelAllTasks(); } } /** * Causes the task referenced by the supplied argument to be cancelled. * If the referenced task is currently executing, it will continue until * finished but will not be rescheduled. * * @param task a task reference */ public static void cancel(final Object task) { if (task instanceof Task) { ((Task) task).cancel(); } } /** * Retrieves whether the specified argument references a cancelled task. * * @param task a task reference * @return true if referenced task is cancelled */ public static boolean isCancelled(final Object task) { return (task instanceof Task) ? ((Task) task).isCancelled() : true; } /** * Retrieves whether the specified argument references a task scheduled * periodically using fixed rate scheduling. * * @param task a task reference * @return true if the task is scheduled at a fixed rate */ public static boolean isFixedRate(final Object task) { if (task instanceof Task) { final Task ltask = (Task) task; return (ltask.relative && ltask.period > 0); } else { return false; } } /** * Retrieves whether the specified argument references a task scheduled * periodically using fixed delay scheduling. * * @param task a task reference * @return true if the reference is scheduled using a fixed delay */ public static boolean isFixedDelay(final Object task) { if (task instanceof Task) { final Task ltask = (Task) task; return (!ltask.relative && ltask.period > 0); } else { return false; } } /** * Retrieves whether the specified argument references a task scheduled * for periodic execution. * * @param task a task reference * @return true if the task is scheduled for periodic execution */ public static boolean isPeriodic(final Object task) { return (task instanceof Task) ? (((Task) task).period > 0) : false; } /** * Retrieves the last time the referenced task was executed, as a * Date object. If the task has never been executed, null is returned. * * @param task a task reference * @return the last time the referenced task was executed; null if never */ public static Date getLastScheduled(Object task) { if (task instanceof Task) { final Task ltask = (Task) task; final long last = ltask.getLastScheduled(); return (last == 0) ? null : new Date(last); } else { return null; } } /** * Sets the periodicity of the designated task to a new value.

* * If the designated task is cancelled or the new period is identical to the * task's current period, then this invocation has essentially no effect * and the submitted object is returned.

* * Otherwise, if the new period is greater than the designated task's * current period, then a simple assignment occurs and the submitted * object is returned.

* * If neither case holds, then the designated task is cancelled and a new, * equivalent task with the new period is scheduled for immediate first * execution and returned to the caller.

* * @return a task reference, as per the rules stated above. * @param task the task whose periodicity is to be set * @param period the new period */ public static Object setPeriod(final Object task, final long period) { return (task instanceof Task) ? ((Task) task).setPeriod(period) : task; } /** * Retrieves the next time the referenced task is due to be executed, as a * Date object. If the referenced task is cancelled, null is returned. * * @param task a task reference * @return the next time the referenced task is due to be executed */ public static Date getNextScheduled(Object task) { if (task instanceof Task) { final Task ltask = (Task) task; final long next = ltask.isCancelled() ? 0 : ltask.getNextScheduled(); return next == 0 ? null : new Date(next); } else { return null; } } /** * Adds to the task queue a new Task object encapsulating the supplied * Runnable and scheduling arguments. * * @param first the time of the task's first execution * @param runnable the Runnable to execute * @param period the task's periodicity * @param relative if true, use fixed rate else use fixed delay scheduling * @return an opaque reference to the internal task */ protected Task addTask(final long first, final Runnable runnable, final long period, boolean relative) { if (this.isShutdown) { throw new IllegalStateException("shutdown"); } final Task task = new Task(first, runnable, period, relative); // sychronized this.taskQueue.addTask(task); // sychronized this.restart(); return task; } /** Sets the background thread to null. */ protected synchronized void clearThread() { try { taskRunnerThread.setContextClassLoader(null); } catch (Throwable t) {} taskRunnerThread = null; } /** * Retrieves the next task to execute, or null if this timer is shutdown, * the current thread is interrupted, or there are no queued tasks. * * @return the next task to execute, or null */ protected Task nextTask() { try { while (!this.isShutdown || Thread.interrupted()) { long now; long next; long wait; Task task; // synchronized to ensure removeTask // applies only to the peeked task, // when the computed wait <= 0 synchronized (this.taskQueue) { task = this.taskQueue.peekTask(); if (task == null) { // queue is empty break; } now = System.currentTimeMillis(); next = task.next; wait = (next - now); if (wait > 0) { // release ownership of taskQueue monitor and await // notification of task addition or cancellation, // at most until the time when the peeked task is // next supposed to execute this.taskQueue.park(wait); continue; // to top of loop } else { this.taskQueue.removeTask(); } } long period = task.period; if (period > 0) { // repeated task if (task.relative) { // using fixed rate scheduling final long late = (now - next); if (late > period) { // ensure that really late tasks don't // completely saturate the head of the // task queue period = 0; /** @todo : is -1, -2 ... fairer? */ } else if (late > 0) { // compensate for scheduling overruns period -= late; } } task.updateSchedule(now, now + period); this.taskQueue.addTask(task); } return task; } } catch (InterruptedException e) { //e.printStackTrace(); } return null; } /** * stats var */ static int nowCount = 0; /** * Convenience method replacing the longer incantation: * System.currentTimeMillis() * * @return System.currentTimeMillis() */ static long now() { nowCount++; return System.currentTimeMillis(); } /** * The Runnable that the background thread uses to execute * scheduled tasks.

* * Note: Outer class could simply implement Runnable, * but using an inner class protects the public run method * from potential abuse. */ protected class TaskRunner implements Runnable { /** * Runs the next available task in the background thread.

* * When there are no available tasks, the background * thread dies and its instance field is cleared until * tasks once again become available. */ public void run() { try { do { final Task task = HsqlTimer.this.nextTask(); if (task == null) { break; } // PROBLEM: If the runnable throws an exception other // than InterruptedException (which likely stems // naturally from calling shutdownImmediately() // or getThread().interrupt()), this will still // cause the loop to exit, which is to say that // task scheduling will stop until a new task is // added or the timer is restarted directly, even // though there may still be uncancelled tasks // left on the queue. // // TODO: Clarify and establish a contract regarding // the difference between InterruptedException, // RuntimeException and other things, like // UndeclaredThrowableException. // // SOL'N: At present, we simply require each runnable to // understand its part of the implicit contract, // which is to deal with exceptions internally // (not throw them up to the timer), with the // possible exception of InterruptedException. // // If the integrity of work performed by the // runnable may be adversely affected by an // unplanned interruption, the runnable should // deal with this directly, for instance by // catching the InterruptedException, ensuring // that some integrity preserving state is // attained, and then rethrowing the exception. task.runnable.run(); } while (true); } finally { HsqlTimer.this.clearThread(); } } } /** * Encapsulates a Runnable and its scheduling attributes. * * Essentially, a wrapper class used to schedule a Runnable object * for execution by the enclosing HsqlTimer's TaskRunner in a * background thread. */ protected class Task { /** What to run. */ Runnable runnable; /** The periodic interval, or 0 if one-shot. */ long period; /** The time this task was last executed, or 0 if never. */ long last; /** The next time this task is scheduled to execute. */ long next; /** * Whether to silently remove this task instead of running it, * the next time (if ever) it makes its way to the head of the * timer queue. */ boolean cancelled = false; /** Serializes concurrent access to the cancelled field. */ private final Object cancel_mutex = new Object(); /** * Scheduling policy flag.

* * When true, scheduling is fixed rate (as opposed to fixed delay), * and schedule updates are calculated relative to when the task was * was last run rather than a fixed delay starting from the current * wall-clock time provided by System.currentTimeMillis().

* * This helps normalize scheduling for tasks that must attempt to * maintain a fixed rate of execution. */ final boolean relative; /** * Constructs a new Task object encapsulating the specified Runnable * and scheduling arguments. * * @param first the first time to execute * @param runnable the Runnable to execute * @param period the periodicity of execution * @param relative if true, use fixed rate scheduling else fixed delay */ Task(final long first, final Runnable runnable, final long period, final boolean relative) { this.next = first; this.runnable = runnable; this.period = period; this.relative = relative; } // fixed reported race condition /** Sets this task's cancelled flag true and signals its taskQueue. */ void cancel() { boolean signalCancelled = false; synchronized (cancel_mutex) { if (!cancelled) { cancelled = signalCancelled = true; } } if (signalCancelled) { HsqlTimer.this.taskQueue.signalTaskCancelled(this); } } /** * Retrieves whether this task is cancelled. * * @return true if cancelled, else false */ boolean isCancelled() { synchronized (cancel_mutex) { return cancelled; } } /** * Retrieves the instant in time just before this task was * last executed by the background thread. A value of zero * indicates that this task has never been executed. * * @return the last time this task was executed or zero if never */ synchronized long getLastScheduled() { return last; } /** * Retrieves the time at which this task is next scheduled for * execution. * * @return the time at which this task is next scheduled for * execution */ synchronized long getNextScheduled() { return next; } /** * Updates the last and next scheduled execution times. * * @param last when this task was last executed * @param next when this task is to be next executed */ synchronized void updateSchedule(final long last, final long next) { this.last = last; this.next = next; } /** * Sets the new periodicity of this task in milliseconds.

* * If this task is cancelled or the new period is identical to the * current period, then this invocation has essentially no effect * and this object is returned.

* * Otherwise, if the new period is greater than the current period, then * a simple field assignment occurs and this object is returned.

* * If none of the previous cases hold, then this task is cancelled and * a new, equivalent task with the new period is scheduled for * immediate first execution and returned to the caller.

* * @param newPeriod the new period * @return a task reference, as per the rules stated above. */ synchronized Object setPeriod(final long newPeriod) { if (this.period == newPeriod || this.isCancelled()) { return this; } else if (newPeriod > this.period) { this.period = newPeriod; return this; } else { this.cancel(); return HsqlTimer.this.addTask(now(), this.runnable, newPeriod, this.relative); } } } /** * Heap-based priority queue. * * Provides extensions to facilitate and simplify implementing * timer functionality. */ protected static class TaskQueue extends HsqlArrayHeap { /** * Constructs a new TaskQueue with the specified initial capacity and * ObjectComparator. * * @param capacity the initial capacity of the queue * @param oc The ObjectComparator this queue uses to maintain its * Heap invariant. */ TaskQueue(final int capacity, final Comparator oc) { super(capacity, oc); } /** * Type-safe add method.

* * Can be used to inject debugging or accounting behaviour.

* * @param task the task to add */ void addTask(final Task task) { // System.out.println("task added: " + task); super.add(task); } /** * Atomically removes all tasks in this queue and then and cancels * them. */ void cancelAllTasks() { Object[] oldHeap; int oldCount; synchronized (this) { oldHeap = this.heap; oldCount = this.count; // 1 instead of 0 to avoid unintended aoob exceptions this.heap = new Object[1]; this.count = 0; } for (int i = 0; i < oldCount; i++) { ((Task) oldHeap[i]).cancelled = true; } } /** * Causes the calling thread to wait until another thread invokes * {@link #unpark() unpark} or the specified amount of time has * elapsed. * * Implements the sync & wait(n) half of this queue's availability * condition.

* * @param timeout the maximum time to wait in milliseconds. * @throws java.lang.InterruptedException if another thread has * interrupted the current thread. The interrupted status of * the current thread is cleared when this exception is thrown. */ synchronized void park(final long timeout) throws InterruptedException { this.wait(timeout); } /** * Retrieves the head of this queue, without removing it.

* * This method has the side-effect of removing tasks from the * head of this queue until a non-cancelled task is encountered * or this queue is empty.

* * If this queue is initially empty or is emptied in the process * of finding the earliest scheduled non-cancelled task, * then null is returned.

* * @return the earliest scheduled non-cancelled task, or null if no such * task exists */ synchronized Task peekTask() { while (super.heap[0] != null && ((Task) super.heap[0]).isCancelled()) { super.remove(); } return (Task) super.heap[0]; } /** * Informs this queue that the given task is supposedly cancelled.

* * If the indicated task is identical to the current head of * this queue, then it is removed and this queue is * {@link #unpark() unparked}.

* * The cancelled status of the given task is not verified; it is * assumed that the caller is well-behaved (always passes a * non-null reference to a cancelled task). * * @param task a supposedly cancelled task */ synchronized void signalTaskCancelled(Task task) { // We only care about the case where HsqlTimer.nextTask // might be parked momentarily on this task. if (task == super.heap[0]) { super.remove(); this.notify(); } } /** * Type-safe remove method.

* * Removes the head task from this queue.

* * Can be used to inject debugging or accounting behaviour.

* * @return this queue's head task or null if no such task exists */ Task removeTask() { // System.out.println("removing task..."); return (Task) super.remove(); } /** * Wakes up a single thread (if any) that is waiting on this queue's * {@link #park(long) park} method. * * Implements the sync & notify half of this queue's availability * condition. */ synchronized void unpark() { this.notify(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy