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

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





© 2015 - 2024 Weber Informatics LLC | Privacy Policy