net.sf.eBus.timer.EScheduledExecutor Maven / Gradle / Ivy
//
// Copyright 2024 Charles W. Rapp
//
// Licensed 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 net.sf.eBus.timer;
import com.google.common.base.Strings;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import net.sf.eBus.client.EClient;
import net.sf.eBus.client.EObject;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.config.EConfigure.ScheduledExecutor;
import static net.sf.eBus.config.ThreadType.SPINYIELD;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Executes given task, {@link EObject} pairs at a specified
* time. When task timer expires, the task and {@code EObject}
* are {@link EClient#dispatch(Runnable, EObject) dispatched}
* via the eBus run queue.
*
* Schedule methods are used to create timers with various
* delays, returning a timer object which can be used to
* cancel the timer or check its status. Methods
* {@link #scheduleAtFixedRate(Runnable, EObject, Duration, Duration)}
* and
* {@link #scheduleWithFixedDelay(Runnable, EObject, Duration, Duration)}
* create and execute timer tasks which run periodically until
* canceled.
*
*
* Unlike {@code java.util.concurrent.ScheduledExecutorService},
* eBus scheduled executor does not support delay or
* period < zero. A repeating fixed delay or repeating fixed
* rate period must be > zero. A zero single-shot delay or
* initial delay must be ≥ zero.
*
*
* All scheduled methods accept relative delays and
* periods as arguments and not absolute times or date.
*
* Creating an EScheduledTimer
* eBus scheduled timers can be created in two ways: one at start
* up by defining them in
* {@code -Dnet.sf.eBus.config.jsonFile= scheduledExecutors}
* block or programmatically using
* {@code ScheduleExecutorBuilder} to create a
* {@code ScheduledExecutor} instance which is then passed to
* {@link #newScheduledExecutor(EConfigure.ScheduledExecutor)}.
* This method returns the {@code EScheduledExecutor} instance
* based on the given configuration. See
* {@link net.sf.eBus.config.EConfigure.ScheduledExecutor} for
* details on configuring an eBus scheduled executor.
*
* Note eBus scheduled executors are required to have a unique
* name. This allows eBus scheduled executor instances to be
* retrieved using {@link #getExecutor(String)}. An attempt to
* create an executor with a named equaling an existing
* executor's will result in an exception.
*
* Usage Example
* This code example assumes that an eBus scheduled executor
* named "fast-timer" has already been created.
* import java.time.Duration;
import java.time.Instant;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.EFeedState;
import net.sf.eBus.client.EScheduledExecutor;
import net.sf.eBus.client.ESubscribeFeed;
import net.sf.eBus.client.ESubscriber;
import net.sf.eBus.client.IESubscribeFeed;
import net.sf.eBus.client.IETimer;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
public class MySubscriber implements ESubscriber {
private static final String EXECUTOR_NAME = "fast-timer";
// Verify that at least one update is received over the past minute.
private static final Duration TIMER_DELAY = Duration.ofMinutes(1L);
private final EMessageKey mKey;
private final FeedScope mScope;
private ESubscribeFeed mFeed;
private Instant mLatestUpdate;
private IETimer mUpdateTimer;
public MySubscriber(final String subject, final FeedScope scope) {
mKey = new EMessageKey(AppUpdate.class, subject);
mScope = scope;
}
@Override public void startup() {
mFeed = (ESubscribeFeed.builder()).target(this)
.messageKey(mKey)
.scope(mScope)
.statusCallback(this::onFeedStatus)
.notifyCallback(this::onUpdate)
.build();
mFeed.subscribe();
}
@Override public void shutdown() {
if (mFeed != null) {
mFeed.close();
mFeed = null;
}
stopTimer();
}
private void onFeedStatus(final EFeedState state, final IESubscribeFeed feed) {
// If the feed is now down, then stop the timer.
if (state == EFeedState.DOWN) {
stopTimer();
} else {
final EScheduleExecutor executor = EScheduledExecutor.getExecutor(EXECUTOR_NAME);
mUpdateTimer = executor.scheduleWithFixedDelay(this::onTimeout, this, TIMER_DELAY, TIMER_DELAY);
mLatestUpdate = Instant.now();
}
}
private void onUpdate(final AppUpdate update) {
mLatestUpdate = update.timestampAsInstant();
...
}
private void onTimeout() {
final Duration delta = Duration.between(mLatestUpdate, Instant.now());
// Did an update arrive within the last minute?
if (delta.compareTo(TIMER_DELAY) > 0) {
// Yes. Take necessary actions.
...
}
}
private void stopTimer() {
if (mUpdateTimer != null) {
try {
mTimer.close();
} catch (Exception jex) {
// Do nothing.
} finally {
mTimer = null;
mLatestUpdate = null;
}
}
}
}
*
* @see EScheduledExecutorBlocking
* @see EScheduledExecutorSpinning
* @see EScheduledExecutorSpinSleep
* @see EScheduledExecutorSpinYield
* @see net.sf.eBus.config.EConfigure.ScheduledExecutor
* @see net.sf.eBus.config.EConfigure.ScheduledExecutorBuilder
*
* @author Charles W. Rapp
*/
public abstract class EScheduledExecutor
extends Thread
{
//---------------------------------------------------------------
// Member enums.
//
/**
* An eBus-scheduled timer may be in one of three states:
*
* -
* waiting for an active timer to expire,
*
* -
* timer has expired and is no longer active, and
*
* -
* timer is canceled, will never expire, and is no longer
* active.
*
*
*/
public enum TimerState
{
/**
* Waiting for active timer to expire.
*/
ACTIVE (true),
/**
* Timer successfully reached expiration and is no longer
* active.
*/
EXPIRED (false),
/**
* Timer is canceled, will never expire, and is no longer
* active.
*/
CANCELED (false);
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Set to {@code true} if state means timer has yet to
* expire or be canceled.
*/
private final boolean mIsActive;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private TimerState(final boolean isActive)
{
mIsActive = isActive;
} // end of TimerState(boolean)
//
// end of Constructors
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
public boolean isActive()
{
return (mIsActive);
} // end of isActive()
//
// end of Get Methods.
//-------------------------------------------------------
} // end of enum TimerState
/**
* Enumerates supported timer type: single shot, repeated at
* a fixed rate, and repeated at a fixed delay.
*/
public enum TimerType
{
/**
* Timer expires only once and then is permanently
* inactive.
*/
SINGLE_SHOT,
/**
* Timer repeats at a fixed rate based on when the timer
* was initially scheduled. Subsequent expirations will
* be scheduled on the initial timestamp + (n * period).
* This timer is best used for "alarm clock" timers which
* need to expire at a certain wall clock time.
*/
REPEAT_FIXED_RATE,
/**
* Timer repeats at fixed delay based on when timer
* expires. Subsequence expirations will be scheduled
* on the current time + periodic delay. This timer is
* best used for events which must occur at a fixed delay
* independent of wall clock time.
*/
REPEAT_FIXED_DELAY;
} // end of enum TimerType
/**
* Provides reason for a timer no longer running.
*/
public enum CancelReason
{
/**
* Timer is still running.
*/
NOT_CANCELED,
/**
* A single-shot timer is no longer running due to its
* sole expiration.
*/
SINGLE_SHOT_TIMER_EXPIRED,
/**
* Timer canceled due to user request.
*/
USER_REQUEST,
/**
* Timer canceled due to associated timer task throwing
* an exception.
*/
TASK_EXCEPTION,
/**
* Timer canceled due to associated executor shut down.
*/
EXECUTOR_SHUTDOWN
} // end of enum CancelReason
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
private static final String NULL_TASK = "task is null";
private static final String NULL_CLIENT = "client is null";
private static final String NULL_DELAY = "delay is null";
private static final String NULL_INIT_DELAY =
"initial delay is null";
private static final String NULL_PERIOD = "period is null";
private static final String NEGATIVE_DELAY = "delay < zero";
private static final String NEGATIVE_INIT_DELAY =
"initial delay < zero";
private static final String NEGATIVE_PERIOD =
"period <= zero";
private static final String EXEC_SHUT_DOWN =
"scheduled executor is shut down";
//-----------------------------------------------------------
// Statics.
//
/**
* Maps unique scheduled executor name to its instance.
*/
private static final Map sScheduledExecutors =
new ConcurrentHashMap<>();
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
LoggerFactory.getLogger(EScheduledExecutor.class);
//-----------------------------------------------------------
// Locals.
//
/**
* Contains active timers yet to expire.
*/
protected final ConcurrentSkipListSet mTimers;
/**
* Continue running while this flag is {@code true}.
*/
protected volatile boolean mRunFlag;
/**
* Decrement this signal when thread is started.
*/
private CountDownLatch mStartupSignal;
/**
* Decrement this signal when thread is stopped.
*/
private final CountDownLatch mShutdownSignal;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new eBus scheduled executor instance.
* @param config contains scheduled executor configuration.
* @param startupSignal decrement signal when scheduled
* executor thread is up and running.
* @param shutdownSignal decrement signal when scheduled
* executor thread is shut down.
*/
protected EScheduledExecutor(final ScheduledExecutor config,
final CountDownLatch startupSignal,
final CountDownLatch shutdownSignal)
{
super (config.name());
mTimers = new ConcurrentSkipListSet<>();
mRunFlag = false;
mStartupSignal = startupSignal;
mShutdownSignal = shutdownSignal;
} // end of EScheduledExecutor(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Adds a new eBus timer to timers set in such a way as to
* notify {@link #waitForExpiration(ETimerImpl)} if
* the new timer expires before the current next-to-expire
* timer.
* @param timer new timer to be added to timers set.
*/
protected abstract void addTimer(ETimerImpl timer);
/**
* Removes given timer from timers set in such a way as to
* notify {@link #waitForExpiration(ETimerImpl)} if
* removed timer is currently next to expire.
* @param timer timer removed from timers set.
*/
protected abstract void removeTimer(ETimerImpl timer);
/**
* Returns next eBus timer scheduled to expire. Will return
* {@code null} if this scheduled executor is stopped.
* @return next timer scheduled to expire or {@code null} if
* executor is stopped.
*/
protected abstract @Nullable ETimerImpl pollTimer();
/**
* Returns {@code true} if given eBus timer expired and
* {@code false} if the routine was preempted by either this
* scheduled timer stopping or a new timer was scheduled and
* is due to expire before this timer.
* @param timer next timer to expire.
* @return {@code true} if {@code timer} expires and
* {@code false} if not.
*/
protected abstract boolean waitForExpiration(ETimerImpl timer);
//
// end of Abstract Method Declarations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Thread Method Overrides.
//
@Override
public final void run()
{
ETimerImpl timer;
mRunFlag = true;
// Decrement start up signal to let creator know this
// thread is up and running.
mStartupSignal.countDown();
mStartupSignal = null;
// Continue processing timers until this scheduled
// executor is stopped.
while (mRunFlag)
{
// Get the next scheduled timer to expire. Wait if
// necessary when there are no scheduled timers.
timer = pollTimer();
// Wait for timer to expire or executor to shut down.
// Note: pollTimer() returns null when run flag is
// false.
if (timer != null &&
mRunFlag &&
waitForExpiration(timer))
{
dispatchExpiredTimers();
}
}
// Cancel all remaining timers and remove from timers
// set.
cancelAllTimers();
// Decrement shutdown signal to let shutdown() method
// know this thread is dead.
mShutdownSignal.countDown();
} // end of run()
//
// end of Thread Method Overrides.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns {@code true} if this eBus scheduled executor is
* running and {@code false} if shutdown.
* @return {@code true} if this executor is running.
*/
public boolean isRunning()
{
return (mRunFlag);
} // end of isRunning()
/**
* Returns {@code true} if eBus scheduled executor with the
* given name exists and {@code false} otherwise.
* @param name eBus scheduled executor name.
* @return {@code true} if eBus scheduled executor named
* {@code name} exists.
*/
public static boolean isExecutor(final String name)
{
return (sScheduledExecutors.containsKey(name));
} // end of isExecutor(String)
/**
* Returns scheduled executor associated with the given name.
* Returns {@code null} if there is no scheduled executor
* associated with the name.
* @param name scheduled executor name.
* @return scheduled executor named {@code name}.
* @throws IllegalArgumentException
* if {@code name} is {@code null} or an empty string.
*/
public static @Nullable EScheduledExecutor getExecutor(final String name)
{
if (Strings.isNullOrEmpty(name))
{
throw (
new IllegalArgumentException(
"name is null or empty"));
}
return (sScheduledExecutors.get(name));
} // end of getExecutor(String)
//
// end of Get Methods.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Set Methods.
//
/**
* Shuts down this executor thread which results in all
* currently scheduled timers being canceled.
*/
public void shutdown()
{
// Set run flag to false and then wake up this thread
// so it returns to main run() while loop.
mRunFlag = false;
signalShutdown();
// Wait here for thread to stop.
try
{
mShutdownSignal.await();
}
catch (InterruptedException interrupt)
{}
} // end of shutdown()
//
// end of Set Methods.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Schedule Methods.
//
/**
* Submits a single-shot task which expires after the given
* delay.
* @param task execute this task when timer expires.
* @param eobject dispatch task to this eBus client's queue.
* @param delay timer expires after this delay.
* @return an {@code IETimer} instance which can be used to
* cancel the timer prior to execution.
* @throws NullPointerException
* if {@code task}, {@code eobject}, or {@code delay} is
* {@code null}.
* @throws RejectedExecutionException
* if {@code delay} < zero.
* @throws IllegalStateException
* if executor is shut down.
*/
public IETimer schedule(final Runnable task,
final EObject eobject,
final Duration delay)
{
final long expiration;
final SingleShotTimer retval;
// Make sure parameters are not null.
Objects.requireNonNull(task, NULL_TASK);
Objects.requireNonNull(eobject, NULL_CLIENT);
Objects.requireNonNull(delay, NULL_DELAY);
// Make sure delay is >= zero.
if (delay.compareTo(Duration.ZERO) < 0)
{
throw (
new RejectedExecutionException(NEGATIVE_DELAY));
}
if (!mRunFlag)
{
throw (new IllegalStateException(EXEC_SHUT_DOWN));
}
final EClient client =
EClient.findOrCreateClient(
eobject, EClient.ClientLocation.LOCAL);
expiration = (System.nanoTime() + delay.toNanos());
// Create timer task and store in timer priority queue.
retval =
new SingleShotTimer(task, client, expiration);
client.addTimer(retval);
addTimer(retval);
return (retval);
} // end of scheduleAtFixedRate(Runnable, EObject, Duration)
/**
* Submits a periodic task which expires for the first time
* after the initial delay and then repeatedly at the
* periodic rate. In other words, expirations are
* {@code initialDelay}, then {@code initialDelay + period},
* then {@code initialDelay + (2 * period)}, and so on.
*
* The given task will continue to be indefinitely executed
* until one of the following occurs:
*
*
* -
* The task is explicitly canceled via the returned
* {@link IETimer} instance.
*
* -
* The client is garbage collected.
*
* -
* The executor is terminated which results in all
* scheduled tasks being canceled.
*
* -
* The task's execution results in a thrown exception.
*
*
* Once a timer is canceled, subsequent executions are
* suppressed and {@link IETimer#isDone() isDone} returns
* {@code true}.
*
* If any task execution takes longer than its period, then
* subsequent executions may start late but will not
* result in multiple scheduled expirations.
*
* @param task execute this task when timer expires.
* @param eobject dispatch task to this eBus client's queue.
* @param initialDelay timer first expiration after this
* delay.
* @param period timer subsequent expirations after this
* period.
* @return {@code IETimer} instance which can be used to
* cancel the timer prior to execution.
*/
public IETimer scheduleAtFixedRate(final Runnable task,
final EObject eobject,
final Duration initialDelay,
final Duration period)
{
final long expiration;
final FixedRateTimer retval;
// Make sure parameters are not null.
Objects.requireNonNull(task, NULL_TASK);
Objects.requireNonNull(eobject, NULL_CLIENT);
Objects.requireNonNull(
initialDelay, NULL_INIT_DELAY);
Objects.requireNonNull(period, NULL_PERIOD);
// Make sure initial delay is >= zero.
if (initialDelay.compareTo(Duration.ZERO) < 0)
{
throw (
new RejectedExecutionException(
NEGATIVE_INIT_DELAY));
}
// Make sure period is > zero.
if (period.compareTo(Duration.ZERO) <= 0)
{
throw (
new RejectedExecutionException(NEGATIVE_PERIOD));
}
if (!mRunFlag)
{
throw (new IllegalStateException(EXEC_SHUT_DOWN));
}
final EClient client =
EClient.findOrCreateClient(
eobject, EClient.ClientLocation.LOCAL);
expiration =
(System.nanoTime() + initialDelay.toNanos());
// Create timer task and store in timer priority queue.
retval =
new FixedRateTimer(task, client, expiration, period);
client.addTimer(retval);
addTimer(retval);
return (retval);
} // end of scheduleAtFixedRate(Runnable, EObject, Duration, Duration)
/**
* Submits a periodic task which expires for the first time
* after the initial delay and then repeatedly with the given
* delay between the termination of the previous expiration
* and the commencement of the next. This means that task
* execution time does not impact scheduling the subsequent
* expirations. When the task completes, the next expiration
* is current time plus delay.
*
* The given task will continue to be indefinitely executed
* until one of the following occurs:
*
*
* -
* The task is explicitly canceled via the returned
* {@link IETimer} instance.
*
* -
* The client is garbage collected.
*
* -
* The executor is terminated which results in all
* scheduled tasks being canceled.
*
* -
* The task's execution results in a thrown exception.
*
*
* Once a timer is canceled, subsequent executions are
* suppressed and {@link IETimer#isDone() isDone} returns
* {@code true}.
* @param task execute this task when timer expires.
* @param eobject dispatch task to this eBus client's queue.
* @param initialDelay timer first expiration after this
* delay.
* @param delay timer subsequence expirations after this
* delay.
* @return {@code IETimer} instance which can be used to
* cancel the timer prior to execution.
*/
public IETimer scheduleWithFixedDelay(final Runnable task,
final EObject eobject,
final Duration initialDelay,
final Duration delay)
{
final long expiration;
final FixedDelayTimer retval;
// Make sure parameters are not null.
Objects.requireNonNull(task, NULL_TASK);
Objects.requireNonNull(eobject, NULL_DELAY);
Objects.requireNonNull(
initialDelay, NULL_INIT_DELAY);
Objects.requireNonNull(delay, NULL_DELAY);
// Make sure initial delay is >= zero.
if (initialDelay.compareTo(Duration.ZERO) < 0)
{
throw (
new RejectedExecutionException(
NEGATIVE_INIT_DELAY));
}
// Make sure repeating delay is > zero.
if (delay.compareTo(Duration.ZERO) <= 0)
{
throw (
new RejectedExecutionException("delay <= zero"));
}
if (!mRunFlag)
{
throw (new IllegalStateException(EXEC_SHUT_DOWN));
}
final EClient client =
EClient.findOrCreateClient(
eobject, EClient.ClientLocation.LOCAL);
expiration =
(System.nanoTime() + initialDelay.toNanos());
// Create timer task and store in timer priority queue.
retval =
new FixedDelayTimer(task, client, expiration, delay);
client.addTimer(retval);
addTimer(retval);
return (retval);
} // end of scheduleWithFixedDelay(...)
//
// end of Schedule Methods.
//-----------------------------------------------------------
/**
* Loads each of the schedule executors contained in
* {@link EConfigure#scheduledExecutors()} map.
* @param config contains schedule executor configurations.
*/
public static void loadScheduledExecutors(final EConfigure config)
{
final Collection configs =
(config.scheduledExecutors()).values();
for (ScheduledExecutor c : configs)
{
newScheduledExecutor(c);
}
} // end of loadScheduledExecutors(EConfigure)
/**
* Returns a new eBus scheduled executor based on the given
* configuration. The returned executor is fully started and
* ready to accept timer scheduling.
* @param config scheduled executor configuration.
* @return new eBus scheduled executor
* @throws IllegalArgumentException
* if {@code config} contains a duplicate scheduled executor
* name.
*/
public static EScheduledExecutor newScheduledExecutor(final ScheduledExecutor config)
{
final CountDownLatch startupSignal =
new CountDownLatch(1);
final CountDownLatch shutdownSignal =
new CountDownLatch(1);
final EScheduledExecutor retval;
switch (config.threadType())
{
case BLOCKING:
retval =
new EScheduledExecutorBlocking(
config, startupSignal, shutdownSignal);
break;
case SPINNING:
retval =
new EScheduledExecutorSpinning(
config, startupSignal, shutdownSignal);
break;
case SPINYIELD:
retval =
new EScheduledExecutorSpinYield(
config, startupSignal, shutdownSignal);
break;
default:
retval =
new EScheduledExecutorSpinSleep(
config, startupSignal, shutdownSignal);
}
sScheduledExecutors.put(retval.getName(), retval);
// Get the scheduled executor thread up and running
// before returning.
retval.start();
try
{
startupSignal.await();
}
catch (InterruptedException interrupt)
{}
return (retval);
}
/**
* Returns next timer scheduled to expire or {@code null} if
* there are no scheduled timers.
* @return next timer scheduled to expire.
*/
protected final @Nullable ETimerImpl nextTimer()
{
ETimerImpl retval = null;
try
{
retval = (ETimerImpl) mTimers.first();
}
catch (NoSuchElementException elemex)
{
// Return null;
}
return (retval);
} // end of nextTimer()
/**
* Dispatches all expired timers in the timers set. Removes
* any expired or canceled timers found along the way.
*/
private void dispatchExpiredTimers()
{
final List repeatTimers = new ArrayList<>();
final Iterator timerIt = mTimers.iterator();
ETimerImpl timer;
long nanoTime;
boolean continueFlag = true;
while (continueFlag && timerIt.hasNext())
{
timer = (ETimerImpl) timerIt.next();
nanoTime = System.nanoTime();
continueFlag = (timer.expiration() < nanoTime);
// Is this timer active?
if (timer.isDone())
{
// No. Remove from the timers set. This timer
// was canceled just prior to expiration and not
// yet removed from the timers set.
timerIt.remove();
}
// Is this timer expired?
else if (continueFlag)
{
// Yes. Dispatch timer task.
timer.dispatch();
// Remove this timer from the timers set. If it
// is a repeating timer it will be placed back
// into the set when it is rescheduled.
timerIt.remove();
// Add all timers to repeating timers list.
// Single shot timer will mark itself expired
// and not be added back to timers set.
repeatTimers.add(timer);
}
}
// Need to reschedule timers outside of the above loop
// since rescheduling updates the timers set - which
// above loop is iterating over.
for (ETimerImpl t : repeatTimers)
{
if (t.reschedule())
{
addTimer(t);
}
}
} // end of dispatchExpiredTimers()
/**
* Cancels all active timers and clears the timers set.
*/
private void cancelAllTimers()
{
final List timers = new ArrayList<>(mTimers);
ETimerImpl eTimer;
mTimers.clear();
for (IETimer t : timers)
{
eTimer = (ETimerImpl) t;
if (!eTimer.isDone())
{
eTimer.setTimerState(
TimerState.CANCELED,
CancelReason.EXECUTOR_SHUTDOWN,
null);
}
}
} // end of cancelAllTimers()
/**
* Interrupts this thread in case timer is currently parked.
*/
private void signalShutdown()
{
this.interrupt();
} // end of signalShutdown()
//---------------------------------------------------------------
// Inner classes.
//
/**
* A scheduled timer task which may be active, expired, or
* canceled. Once a timer is expired or canceled, it will
* never expire again.
*/
public static interface IETimer
extends AutoCloseable,
Comparable
{
/**
* Returns timer current state.
* @return timer current state.
*
* @see #isCanceled()
* @see #isDone()
*/
TimerState timerState();
/**
* Returns timer type.
* @return timer type.
*/
TimerType timerType();
/**
* Returns remaining delay until this timer expires. If
* timer is either expired (and not a repeating timer) or
* if timer is cancelled, then returns
* {@link Duration#ZERO}.
* @return remaining delay until timer expires.
*/
Duration delay();
/**
* Returns {@code true} if this task was successfully
* canceled prior to expiration. Note that this task
* may be inactive due to expiration.
* @return {@code true} if task is canceled.
*
* @see #timerState()
* @see #isDone()
*/
boolean isCanceled();
/**
* Returns reason why timer is no longer running.
* @return timer cancellation reason.
*/
CancelReason cancelReason();
/**
* Returns exception associated with
* {@link #cancelReason()} returning
* {@link CancelReason#TASK_EXCEPTION}. Returns
* {@code null} for any other cancel reason.
* @return task exception.
*/
@Nullable Exception cancelException();
/**
* Returns {@code true} if this task is no longer active.
* This could be due to either expiration or
* cancellation, there is no distinction between the
* two.
* @return {@code true} if task is no longer active.
*
* @see #timerState()
* @see #isCanceled()
*/
boolean isDone();
} // end of interface IETimer
/**
* Implements {@IETimer} instance and contains necessary
* information to execute expire timer task on the associated
* eBus client task queue and cancel timer.
*/
/* package */ abstract class ETimerImpl
implements IETimer
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Specific timer type.
*/
protected final TimerType mType;
/**
* Current timer state.
*/
protected final AtomicReference mTimerState;
/**
* Timer set to expire at this nanosecond time based on
* {@code System.nanoTime()}. This data member is not
* {@code final} because it is updated by repeat timers.
*/
protected long mExpiration;
/**
* When timer expires, execute this task.
*/
private final Runnable mTask;
/**
* eBus client setting this timer.
*/
private final EClient mClient;
/**
* Reason for timer no longer running.
*/
private CancelReason mCxlReason;
/**
* Timer task exception is thrown.
*/
private Exception mCxlException;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
protected ETimerImpl(final Runnable task,
final EClient client,
final long expiration,
final TimerType type)
{
mTask = task;
mClient = client;
mExpiration = expiration;
mType = type;
mTimerState =
new AtomicReference<>(TimerState.ACTIVE);
} // end of ETimerImple(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Reschedules a repeat timer. Returns {@code true} if
* this is a repeat timer and {@code false} if
* single-shot.
* @return {@code true} if timer re-scheduled.
*/
protected abstract boolean reschedule();
//
// end of Abstract Method Declarations.
//-------------------------------------------------------
//-------------------------------------------------------
// IETimer Interface Implementation.
//
@Override
public final TimerState timerState()
{
return (mTimerState.get());
} // end of timerState()
@Override
public final TimerType timerType()
{
return (mType);
} // end of timerType()
/**
* Returns active timer expiration. Returns
* {@link Duration#ZERO} if timer is inactive.
* @return time until timer expiration.
*/
@Override
public final Duration delay()
{
final long delta = (mExpiration - System.nanoTime());
final Duration retval;
// Is this timer expired or canceled?
if (mTimerState.get() != TimerState.ACTIVE ||
delta <= 0L)
{
// Yes. Return zero.
retval = Duration.ZERO;
}
else
{
retval = Duration.ofNanos(delta);
}
return (retval);
} // end of delay()
@Override
public final boolean isCanceled()
{
return (mTimerState.get() == TimerState.CANCELED);
} // end of isCanceled()
@Override
public final boolean isDone()
{
return (!((mTimerState.get()).isActive()));
} // end of isDone()
/**
* Cancels running timer
* @throws Exception
*/
@Override
public final void close()
throws Exception
{
// Was this timer still active when canceled?
if (mTimerState.compareAndSet(TimerState.ACTIVE,
TimerState.CANCELED))
{
// Yes. Remove this timer from timer set.
removeTimer(this);
setTimerState(TimerState.CANCELED,
CancelReason.USER_REQUEST,
null);
}
} // end of close()
@Override
public final CancelReason cancelReason()
{
return (mCxlReason);
} // end of cancelReason()
@Override
public final @Nullable Exception cancelException()
{
return (mCxlException);
} // end of cancelException()
//
// end of IETimer Interface Implementation.
//-------------------------------------------------------
//-------------------------------------------------------
// Comparable Interface Implementation.
//
/**
* Returns value <, equal to, or greater than zero
* based on whether {@code this IETimer}'s expiration is
* <, equal to; or greater than {@code timer}'s
* expiration.
* @param timer compared timer.
* @return integer values <, equal to, or > zero.
*/
@Override
public final int compareTo(final IETimer timer)
{
final ETimerImpl eTimer = (ETimerImpl) timer;
return (
Long.compare(mExpiration, eTimer.expiration()));
} // end of compareTo(IETimer)
//
// end of Comparable Interface Implementation.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (
String.format(
"[type=%s, state=%s, delay=%,d]",
mType,
mTimerState.get(),
expiration()));
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns timer's expiration in nanosecond with respect
* to {@code System.nanoTime()}.
* @return timer nanosecond expiration.
*/
/* package */ long expiration()
{
return (mExpiration);
} // end of expiration()
/**
* Returns timer task.
* @return timer task.
*/
private Runnable task()
{
return (mTask);
} // end of task()
//
// end of Get Methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Sets timer to given state.
* @param state next timer state.
* @param reason reason timer is canceled.
* @param optional exception associated with timer
* cancellation.
*/
protected void setTimerState(final TimerState state,
final CancelReason reason,
final @Nullable Exception cxlException)
{
mTimerState.set(state);
mCxlReason = reason;
mCxlException = cxlException;
if (!state.isActive())
{
mClient.removeTimer(this);
}
} // end of setTimerState(TimerState)
/**
* Automatically cancels timer due to a timer task
* exception.
* @param cxlException timer task exception.
*/
private void close(final Exception cxlException)
{
// Was this timer still active when canceled?
if (mTimerState.compareAndSet(TimerState.ACTIVE,
TimerState.CANCELED))
{
// Yes. Remove this timer from timer set.
removeTimer(this);
setTimerState(TimerState.CANCELED,
CancelReason.TASK_EXCEPTION,
cxlException);
}
} // end of close(Exception)
//
// end of Set Methods.
//-------------------------------------------------------
/**
* Dispatches client's timer task.
*/
private void dispatch()
{
if (mTimerState.get() == TimerState.ACTIVE)
{
// Put the timer within another timer task so if
// timer throws an exception, the exception can
// be caught and timer canceled.
mClient.dispatch(new TimerTask(this));
}
} // end of dispatch()
} // end of class ETimerImpl
private final class SingleShotTimer
extends ETimerImpl
{
//-----------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private SingleShotTimer(final Runnable task,
final EClient client,
final long expiration)
{
super (task,
client,
expiration,
TimerType.SINGLE_SHOT);
} // end of SingleShotTimer(Runnable, EClient, long)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Implementations.
//
/**
* Does nothing since single-shot timers are not
* rescheduled.
* @return {@code false}.
*/
@SuppressWarnings ({"java:S1186"})
@Override
protected boolean reschedule()
{
// Mark this timer as expired.
setTimerState(TimerState.EXPIRED,
CancelReason.SINGLE_SHOT_TIMER_EXPIRED,
null);
return (false);
} // end of reschedule()
//
// end of Abstract Method Implementations.
//-------------------------------------------------------
} // end of class SingleShotTimer
private final class FixedRateTimer
extends ETimerImpl
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Repeat expirations are based on this expiration,
* period, and expiration count.
*/
private final long mInitialExpiration;
/**
* Subsequent expirations occur at this nanosecond
* period.
*/
private final long mPeriod;
/**
* Tracks number of times this timer has expired. Used
* to calculate next expiration.
*/
private int mExpirationCount;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private FixedRateTimer(final Runnable task,
final EClient client,
final long expiration,
final Duration period)
{
super (task,
client,
expiration,
TimerType.REPEAT_FIXED_RATE);
mInitialExpiration = expiration;
mPeriod = period.toNanos();
mExpirationCount = 0;
} // end of FixedRateTimer(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Implementations.
//
/**
* Schedules the next expiration at a fixed rate.
* @return {@code true} if timer active and expiration
* rescheduled and {@code false} if timer is not active.
*/
@SuppressWarnings ({"java:S1186"})
@Override
protected boolean reschedule()
{
final boolean retcode =
(mTimerState.get()).isActive();
// Is this timer still active?
if (retcode)
{
// Yes. Set the next expiration.
++mExpirationCount;
mExpiration =
(mInitialExpiration +
(mPeriod * mExpirationCount));
}
return (retcode);
} // end of reschedule()
//
// end of Abstract Method Implementations.
//-------------------------------------------------------
} // end of class FixedRateTimer
private final class FixedDelayTimer
extends ETimerImpl
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Subsequent expirations occur at this nanosecond
* period.
*/
private final long mPeriod;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private FixedDelayTimer(final Runnable task,
final EClient client,
final long expiration,
final Duration period)
{
super (task,
client,
expiration,
TimerType.REPEAT_FIXED_RATE);
mPeriod = period.toNanos();
} // end of FixedDelayTimer(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Implementations.
//
/**
* Schedules the next expiration at a fixed rate.
* @return {@code true} if timer active and expiration
* rescheduled and {@code false} if timer is not active.
*/
@SuppressWarnings ({"java:S1186"})
@Override
protected boolean reschedule()
{
final boolean retcode =
(mTimerState.get()).isActive();
// Is this timer still active?
if (retcode)
{
// Yes. Set the next expiration based on the
// current nanosecond time.
mExpiration = (System.nanoTime() + mPeriod);
}
return (retcode);
} // end of reschedule()
//
// end of Abstract Method Implementations.
//-------------------------------------------------------
} // end of class FixedDelayTimer
/**
* Encapsulates scheduled timer task and calls
* {@code mTask.run} within this class' {@code run} method.
* These point is that if {@code mTask.run} throws an
* exception, that exception is caught, logged, and the timer
* is canceled if still active.
*/
private static final class TimerTask
implements Runnable
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Expired timer.
*/
private final ETimerImpl mTimer;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new timer task encapsulating given timer.
* @param timer expired timer.
*/
private TimerTask(final ETimerImpl timer)
{
mTimer = timer;
} // end of TimerTask(ETimerImpl)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementation.
//
/**
* Executes encapsulated timer task. If timer task throws
* an exception, then timer is canceled.
*/
@Override
public void run()
{
try
{
(mTimer.task()).run();
}
catch (Exception jex)
{
sLogger.warn(
"timer task exception; canceling timer",
jex);
mTimer.close(jex);
}
} // end of run()
//
// end of Runnable Interface Implementation.
//-------------------------------------------------------
} // end of class TimerTask
} // end of class EScheduledExecutor