net.sf.eBusx.util.ETimer Maven / Gradle / Ivy
//
// Copyright 2012, 2015, 2016 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.eBusx.util;
import com.google.common.base.Strings;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.EFeed;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.EFeedState;
import net.sf.eBus.client.EReplier;
import net.sf.eBus.client.EReplyFeed;
import net.sf.eBus.client.EReplyFeed.ERequest;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.EReplyMessage.ReplyStatus;
import net.sf.eBus.messages.type.DataType;
/**
* This class provides an eBus request/reply interface on top of
* the Java {@link java.util.Timer} class. This eBus interface
* provides applications a uniform event interface: objects
* receive events as messages from the eBus API. If the Java
* timer interface is used directly, then timer events must be
* manually integrated into the event-processing logic.
*
* This interface is not to be preferred if timer tasks are used
* autonomously rather than as events delivered to an eBus
* client. In that case, the Java {@link java.util.TimerTask}
* should be used directly.
*
*
* {@code ETimer} service must be started by an application by
* calling one of the {@code startETimer} methods before
* application objects can successfully subscribe to the timer
* server. {@code ETimer} service is local to the JVM only.
* Requestors cannot access a remote eBus timer service.
*
*
* See {@code net.sf.eBusx.util} package documentation for sample
* code.
*
*
* @author Charles W. Rapp
*/
public final class ETimer
implements EReplier
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* The default timer name is "ETimer".
*/
public static final String DEFAULT_TIMER_NAME = "ETimer";
/**
* The advertised request message subject is "/eBusTimer".
*/
public static final String TIMER_SUBJECT = "/eBusTimer";
/**
* The advertised request message key is
* TimerRequest:/eBusTimer.
*/
public static final EMessageKey TIMER_KEY =
new EMessageKey(TimerRequest.class, TIMER_SUBJECT);
//-----------------------------------------------------------
// Statics.
//
/**
* The singleton ETimer instance.
*/
private static ETimer sInstance = null;
/**
* The singleton lock.
*/
private static final Lock sLock = new ReentrantLock(true);
/**
* Set this signal when either {@link #startup()} completes.
*/
private static final Condition sStartSignal =
sLock.newCondition();
/**
* Set this signal when either {@link #shutdown()} completes.
*/
private static final Condition sStopSignal =
sLock.newCondition();
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
Logger.getLogger((ETimer.class).getName());
//-----------------------------------------------------------
// Locals.
//
/**
* {@code ETimer} name. Used for logging.
*/
private final String mName;
/**
* The Java timer thread.
*/
private final Timer mTimer;
/**
* The currently running timer requests.
*/
private final Map mTasks;
/**
* The timer service feed.
*/
private EReplyFeed mTimerFeed;
/**
* Set this flag to {@code true} when timer is started.
*/
private boolean mRunFlag;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new ETimer instance with the given name and
* daemon flag.
* @param name the timer thread name.
* @param isDaemon {@code true} means the timer is a daemon
* thread.
*/
private ETimer(final String name, final boolean isDaemon)
{
mName = name;
mTimer = new Timer(name, isDaemon);
mTasks = new HashMap<>();
mTimerFeed = null;
mRunFlag = false;
} // end of ETimer(String, boolean)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// EObject Interface Implementation.
//
/**
* Returns {@code ETimer name}.
* @return eBus object name.
*/
@Override
public String name()
{
return (mName);
} // end of name()
/**
* Puts the timer service request advertisement in place.
*/
@Override
public void startup()
{
sLock.lock();
try
{
// Advertise the local-only timer service.
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: opening and advertising %s feed.",
mName,
TIMER_KEY));
}
// "Compile" the timer messages.
DataType.findType(TimerReply.class);
DataType.findType(TimerRequest.class);
mTimerFeed =
(EReplyFeed.builder()).target(this)
.messageKey(TIMER_KEY)
.scope(FeedScope.LOCAL_ONLY)
.build();
mTimerFeed.advertise();
mTimerFeed.updateFeedState(EFeedState.UP);
mRunFlag = true;
sStartSignal.signal();
}
finally
{
sLock.unlock();
}
} // end of startup()
/**
* Cancels all running timers, stops the timer thread and
* retracts the timer service advertisement.
*/
@Override
public void shutdown()
{
sLock.lock();
try
{
if (mTimerFeed != null)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: closing %s feed.",
mName,
TIMER_KEY));
}
mTimerFeed.close();
mTimerFeed = null;
}
synchronized (mTasks)
{
(mTasks.values()).stream()
.forEach(
TimerHandler::shutdownTimer);
mTasks.clear();
}
mTimer.cancel();
mRunFlag = false;
sStopSignal.signal();
}
finally
{
sLock.unlock();
}
} // end of shutdownTimer()
//
// end of EObject Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// EReplier Interface Implementation.
//
/**
* Starts a timer task based on the given request. If the
* timer request is not valid, then sends a generic reply
* with a failed status and an explanation for the failure.
*
* Do not call this method. This method is
* part of the {@link EReplier} interface and is accessed by
* the eBus API only.
* @param request the eBus request instance.
*/
@Override
public void request(final ERequest request)
{
final TimerRequest timerReq =
(TimerRequest) request.request();
// Need to do this calculation ASAP to minimize chance of
// mistakenly declaring a requested time as in the past.
final Duration duration =
(timerReq.time == null ?
Duration.ZERO :
Duration.between(Instant.now(), timerReq.time));
ReplyStatus replyStatus = ReplyStatus.OK_FINAL;
String reason = null;
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: received timer request:%n%s",
mName,
timerReq));
}
// Validate the timer request first.
// Was an expiration specified?
if (timerReq.time == null && timerReq.delay == 0L)
{
// No.
replyStatus = ReplyStatus.ERROR;
reason =
"must specify either an expiration time or delay";
}
// Were both an expiration time and delay specified?
else if (timerReq.time != null && timerReq.delay > 0L)
{
// Yes.
replyStatus = ReplyStatus.ERROR;
reason =
"cannot specify both an expiration time and delay";
}
// If the expiration date is given, is it in the past?
else if (timerReq.time != null &&
duration.compareTo(Duration.ZERO) < 0)
{
// Yes.
replyStatus = ReplyStatus.ERROR;
reason = "expiration time in the past";
}
// If the repeating period is given, is it valid?
else if (timerReq.period < 0L)
{
// Yes.
replyStatus = ReplyStatus.ERROR;
reason = "period < zero";
}
// Otherwise this message is valid.
// If the request proved invalid, then send a generic
// failure reply now.
if (replyStatus == ReplyStatus.ERROR)
{
final EReplyMessage.ConcreteBuilder builder =
(EReplyMessage.ConcreteBuilder)
EReplyMessage.builder();
request.reply(builder.subject((request.key()).subject())
.replyStatus(replyStatus)
.replyReason(reason)
.build());
}
// The request checked out. Create and schedule the timer
// handler.
else
{
final long delay = (timerReq.time == null ?
timerReq.delay :
duration.toMillis());
synchronized (mTasks)
{
mTasks.put(request,
startTask(delay,
timerReq.period,
timerReq.fixedRate,
request));
}
}
} // end of request(ERequest)
/**
* Cancels the timer task associated with this request and
* sends a cancel complete reply.
*
* Do not call this method. This method is
* part of the {@link EReplier} interface and is accessed by
* the eBus API only.
* @param request timer request being canceled.
* @param mayRespond if {@code true} then send back a
* response.
*/
@Override
public void cancelRequest(final ERequest request,
final boolean mayRespond)
{
// Treat cancel requests and terminations the same.
doCancel(request, mayRespond);
} // end of cancelRequest(ERequest, boolean)
//
// end of EReplier Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns {@code true} if this {@code ETimer} is running
* and {@code false} if not.
* @return {@code true} if eBus timer is running.
*/
public boolean isRunning()
{
return (mRunFlag);
} // end of isRunning()
//
// end of Get Methods.
//-----------------------------------------------------------
/**
* Returns {@code true} if the {@code ETimer} instance is
* running and {@code false} otherwise.
* @return {@code true} if the {@code ETimer} instance is
* running.
*/
public static boolean isETimerRunning()
{
boolean retcode;
sLock.lock();
try
{
retcode = (sInstance != null);
}
finally
{
sLock.unlock();
}
return (retcode);
} // end of isETimerRunning()
/**
* Starts the eBus timer service using the default
* {@link #DEFAULT_TIMER_NAME timer name} and the timer thread is
* not run as a daemon.
* @exception IllegalStateException
* if the eBus timer service is already running.
*/
public static void startETimer()
{
startETimer(DEFAULT_TIMER_NAME, false);
} // end of startETimer()
/**
* Starts the eBus timer service using the default
* {@link #DEFAULT_TIMER_NAME timer name} and the given daemon
* flag.
* @param isDaemon {@code true} means that the timer thread
* is run as a daemon.
* @throws IllegalStateException
* if the eBus timer service is already running.
*/
public static void startETimer(final boolean isDaemon)
{
startETimer(DEFAULT_TIMER_NAME, isDaemon);
} // startETiimer(boolean)
/**
* Starts the eBus timer service for the given timer thread
* name. The timer thread is not run as a daemon.
* @param name the timer thread name.
* @throws IllegalArgumentException
* if {@code name} is either {@code null} or empty.
* @throws IllegalStateException
* if the eBus timer service is already running.
*/
public static void startETimer(final String name)
{
startETimer(name, false);
} // end of startETimer(String)
/**
* Starts the eBus timer service for the given timer thread
* name and daemon flag.
* @param name timer thread name.
* @param isDaemon if {@code true}, then the timer is a
* daemon thread.
* @throws IllegalArgumentException
* if {@code name} is either {@code null} or empty.
* @throws IllegalStateException
* if the eBus timer service is already running.
*/
public static void startETimer(final String name,
final boolean isDaemon)
{
if (Strings.isNullOrEmpty(name))
{
throw (
new IllegalArgumentException(
"null or empty name"));
}
sLock.lock();
try
{
if (sInstance != null)
{
throw (
new IllegalStateException(
"ETimer already started"));
}
else
{
startImpl(name, isDaemon);
}
}
finally
{
sLock.unlock();
}
} // end of startETimer(String, boolean)
/**
* Stops the eBus timer service, if running. All running
* timer requests are canceled and requestors are informed.
*
* Does nothing if the timer service is not running.
*/
public static void stopETimer()
{
sLock.lock();
try
{
if (sInstance != null)
{
EFeed.shutdown(sInstance);
// Wait here for the entry to start up to
// complete.
sLogger.finest(
"Waiting for ETimer shutdown to complete.");
while (sInstance.isRunning())
{
try
{
sStopSignal.await();
}
catch (InterruptedException interrupt)
{}
}
sLogger.finest("ETimer shutdown completed.");
sInstance = null;
sLogger.fine("Deleted ETimer singleton.");
}
}
finally
{
sLock.unlock();
}
} // end of stopETimer()
/**
* Performs the actual work of starting the {@code ETimer}
* instance.
* @param name timer thread name.
* @param isDaemon if {@code true}, then the timer is a
* daemon thread.
*/
private static void startImpl(final String name,
final boolean isDaemon)
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"Creating ETimer %s (%s)",
name,
(isDaemon ? "daemon" : "not daemon")));
}
// Create the singleton and have it do advertise
// its service.
sInstance = new ETimer(name, isDaemon);
EFeed.register(sInstance);
EFeed.startup(sInstance);
// Wait here for the entry to start up to
// complete.
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"Waiting for ETimer %s start-up to complete.",
name));
}
while (!sInstance.isRunning())
{
try
{
sStartSignal.await();
}
catch (InterruptedException interrupt)
{}
}
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"ETimer %s start-up completed.",
name));
}
} // end of startImpl()
/**
* Performs the actual work of canceling a timer request.
* @param request cancel this request.
* @param mayRespond if {@code true} then send back a
* response.
*/
private void doCancel(final ERequest request,
final boolean mayRespond)
{
final TimerHandler task;
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: canceling timer request ",
mName,
request.feedId()));
}
// Find the timer task associated with the request and
// and stop it.
synchronized (mTasks)
{
task = mTasks.remove(request);
}
if (task != null)
{
task.cancelTimer();
}
if (mayRespond)
{
final EReplyMessage.ConcreteBuilder builder =
(EReplyMessage.ConcreteBuilder)
EReplyMessage.builder();
request.reply(
builder.subject(request.messageSubject())
.replyStatus(ReplyStatus.CANCELED)
.build());
}
} // end of doCancel(ERequest, boolean)
/**
* Creates the timer task which posts the timer reply,
* schedules the timer, and then returns it.
* @param delay initial timer delay.
* @param period repeating timer period.
* @param isFixedRate {@code true} if this is a fixed rate
* timer.
* @param request the eBus request instance associated with
* the message.
* @return the timer handler instance.
*/
private TimerHandler startTask(final long delay,
final long period,
final boolean isFixedRate,
final ERequest request)
{
final TimerHandler retval;
// Is this a repeating timer?
if (period > 0L)
{
// Yes. Is this a fixed rate timer?
retval = new TimerHandler(request, this, false);
if (isFixedRate)
{
mTimer.scheduleAtFixedRate(
retval, delay, period);
}
// No, this is not fixed rate.
else
{
mTimer.schedule(retval, delay, period);
}
}
else
{
// No, this is a single shot timer.
retval = new TimerHandler(request, this, true);
mTimer.schedule(retval, delay);
}
return (retval);
} // end of startTask(TimerRequest, ERequest)
/**
* Removes the completed timer task from the map.
* @param request the associated timer request.
*/
private void taskDone(final ERequest request)
{
synchronized (mTasks)
{
mTasks.remove(request);
}
} // end of taskDone(ERequest)
//---------------------------------------------------------------
// Inner classes.
//
/**
* A timer handler instance is created for each accepted
* {@link TimerRequest}. This class is responsible for
* sending {@link TimerReply} messages when the timer task
* is executed.
*/
private static final class TimerHandler
extends TimerTask
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Send replies via this eBus request.
*/
private final ERequest mRequest;
/**
* This handler works for this eBus timer service.
*/
private final ETimer mTimer;
/**
* {@code true} if this is a one shot timer and
* {@code false} if on-going.
*/
private final boolean mOneTimeFlag;
/**
* Count up the number of timer expirations.
*/
private int mCount;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new timer request instance for handling
* the given eBus request.
* @param request the eBus request. Post replies to this
* object.
* @param timer the timer instance registered with the
* eBus request.
* @param oneTimeFlag {@code true} if this is a one shot
* timer.
*/
private TimerHandler(final ERequest request,
final ETimer timer,
final boolean oneTimeFlag)
{
mRequest = request;
mTimer = timer;
mOneTimeFlag = oneTimeFlag;
mCount = 0;
} // end of TimerHandler(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// TimerTask Interface Implementation.
//
/**
* Sends the timer reply message. If this is a one shot
* timer, removes this timer handler from the tasks map.
*/
@Override
public void run()
{
if (mRequest.isActive())
{
final ReplyStatus replyStatus =
(mOneTimeFlag ?
ReplyStatus.OK_FINAL :
ReplyStatus.OK_CONTINUING);
synchronized (this)
{
// Send the timer expiration reply to
// requestor. If this is a single shot timer,
// then this is the first and final reply, so
// remove this timer from the map.
mRequest.reply(
TimerReply.builder()
.timerName((mRequest.key()).subject())
.replyStatus(replyStatus)
.sequenceNumber(mCount)
.build());
++mCount;
}
}
if (mOneTimeFlag)
{
mTimer.taskDone(mRequest);
}
} // end of handleTimeout()
//
// end of TimerTaskListener Interface Implementation.
//-------------------------------------------------------
/**
* Cancels the timer task and sends a cancel reply.
*/
public void cancelTimer()
{
this.cancel();
} // end of void cancelTimer()
/**
* Cancels the timer task and informs the requestor that
* the task failed due to the timer service being shut
* down.
*/
public void shutdownTimer()
{
this.cancel();
if (mRequest.isActive())
{
final EReplyMessage.ConcreteBuilder builder =
(EReplyMessage.ConcreteBuilder)
EReplyMessage.builder();
mRequest.reply(
builder.subject((mRequest.key()).subject())
.replyStatus(ReplyStatus.ERROR)
.replyReason("timer service shutdown")
.build());
}
} // end of shutdownTimer()
} // end of class TimerHandler
} // end of class ETimer