/*
* JBoss, Home of Professional Open Source.
* Copyright 2006, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.ejb.txtimer;
// $Id: TimerImpl.java 102438 2010-03-16 01:19:57Z smcgowan@redhat.com $
import java.io.Serializable;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import javax.ejb.EJBException;
import javax.ejb.NoSuchObjectLocalException;
import javax.ejb.TimerHandle;
import javax.ejb.ScheduleExpression;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.Transaction;
import org.jboss.ejb.AllowedOperationsAssociation;
import org.jboss.logging.Logger;
/**
* An implementation of an EJB Timer.
*
* Internally it uses a java.util.Timer and maintains its state in
* a Tx manner.
*
* @author Thomas.Diesler@jboss.org
* @author Dimitris.Andreadis@jboss.org
* @version $Revision: 102438 $
* @since 07-Apr-2004
*/
public class TimerImpl implements javax.ejb.Timer, Synchronization
{
// logging support
private static Logger log = Logger.getLogger(TimerImpl.class);
/**
* Timer states and their allowed transitions
* <p/>
* CREATED - on create
* CREATED -> STARTED_IN_TX - when strated with Tx
* CREATED -> ACTIVE - when started without Tx
* STARTED_IN_TX -> ACTIVE - on Tx commit
* STARTED_IN_TX -> CANCELED - on Tx rollback
* ACTIVE -> CANCELED_IN_TX - on cancel() with Tx
* ACTIVE -> CANCELED - on cancel() without Tx
* CANCELED_IN_TX -> CANCELED - on Tx commit
* CANCELED_IN_TX -> ACTIVE - on Tx rollback
* ACTIVE -> IN_TIMEOUT - on TimerTask run
* IN_TIMEOUT -> ACTIVE - on Tx commit if periode > 0
* IN_TIMEOUT -> EXPIRED -> on Tx commit if periode == 0
* IN_TIMEOUT -> RETRY_TIMEOUT -> on Tx rollback
* RETRY_TIMEOUT -> ACTIVE -> on Tx commit/rollback if periode > 0
* RETRY_TIMEOUT -> EXPIRED -> on Tx commit/rollback if periode == 0
*/
private static final int CREATED = 0;
private static final int STARTED_IN_TX = 1;
private static final int ACTIVE = 2;
private static final int CANCELED_IN_TX = 3;
private static final int CANCELED = 4;
private static final int EXPIRED = 5;
private static final int IN_TIMEOUT = 6;
private static final int RETRY_TIMEOUT = 7;
private static final String[] TIMER_STATES = {"created", "started_in_tx", "active", "canceled_in_tx",
"canceled", "expired", "in_timeout", "retry_timeout"};
// The initial txtimer properties
private TimerServiceImpl timerService;
private String timerId;
private TimedObjectId timedObjectId;
private TimedObjectInvoker timedObjectInvoker;
private Date firstTime;
private long periode;
private Serializable info;
private long nextExpire;
private int timerState;
private Timer utilTimer;
private int hashCode;
/**
* Schedules the txtimer for execution at the specified time with a specified periode.
*/
TimerImpl(TimerServiceImpl timerService, String timerId, TimedObjectId timedObjectId, TimedObjectInvoker timedObjectInvoker, Serializable info)
{
this.timerService = timerService;
this.timerId = timerId;
this.timedObjectId = timedObjectId;
this.timedObjectInvoker = timedObjectInvoker;
this.info = info;
setTimerState(CREATED);
}
void startTimer(Date firstTime, long periode)
{
this.firstTime = firstTime;
this.nextExpire = firstTime.getTime();
this.periode = periode;
timerService.addTimer(this);
registerTimerWithTx();
// the timer will actually go ACTIVE on tx commit
startInTx();
}
public String getTimerId()
{
return timerId;
}
public TimedObjectId getTimedObjectId()
{
return timedObjectId;
}
public Date getFirstTime()
{
return firstTime;
}
public long getPeriode()
{
return periode;
}
public long getNextExpire()
{
return nextExpire;
}
public Serializable getInfoInternal()
{
return info;
}
public boolean isActive()
{
return !isCanceled() && !isExpired();
}
public boolean isInRetry() {
return timerState == RETRY_TIMEOUT;
}
public boolean isCanceled()
{
return timerState == CANCELED_IN_TX || timerState == CANCELED;
}
public boolean isExpired()
{
return timerState == EXPIRED;
}
/**
* Cause the txtimer and all its associated expiration notifications to be cancelled.
*
* @throws IllegalStateException If this method is invoked while the instance is in
* a state that does not allow access to this method.
* @throws javax.ejb.NoSuchObjectLocalException
* If invoked on a txtimer that has expired or has been cancelled.
* @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
*/
public void cancel() throws IllegalStateException, NoSuchObjectLocalException, EJBException
{
assertTimedOut();
assertAllowedOperation("Timer.cancel");
registerTimerWithTx();
cancelInTx();
}
/**
* Kill the timer, and remove it from the timer service
*/
public void killTimer()
{
log.debug("killTimer: " + this);
if (timerState != EXPIRED)
setTimerState(CANCELED);
timerService.removeTimer(this);
utilTimer.cancel();
}
/**
* killTimer w/o persistence work
*/
private void cancelTimer()
{
if (timerState != EXPIRED)
setTimerState(CANCELED);
utilTimer.cancel();
}
/**
* Kill the timer, do not remove from timer service
*/
public void stopTimer()
{
log.debug("stopTimer: " + this);
if (timerState != EXPIRED)
setTimerState(CANCELED);
utilTimer.cancel();
}
/**
* Get the number of milliseconds that will elapse before the next scheduled txtimer expiration.
*
* @return Number of milliseconds that will elapse before the next scheduled txtimer expiration.
* @throws IllegalStateException If this method is invoked while the instance is in
* a state that does not allow access to this method.
* @throws javax.ejb.NoSuchObjectLocalException
* If invoked on a txtimer that has expired or has been cancelled.
* @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
*/
public long getTimeRemaining() throws IllegalStateException, NoSuchObjectLocalException, EJBException
{
assertTimedOut();
assertAllowedOperation("Timer.getTimeRemaining");
return nextExpire - System.currentTimeMillis();
}
/**
* Get the point in time at which the next txtimer expiration is scheduled to occur.
*
* @return Get the point in time at which the next txtimer expiration is scheduled to occur.
* @throws IllegalStateException If this method is invoked while the instance is in
* a state that does not allow access to this method.
* @throws javax.ejb.NoSuchObjectLocalException
* If invoked on a txtimer that has expired or has been cancelled.
* @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
*/
public Date getNextTimeout() throws IllegalStateException, NoSuchObjectLocalException, EJBException
{
assertTimedOut();
assertAllowedOperation("Timer.getNextTimeout");
return new Date(nextExpire);
}
/**
* Get the information associated with the txtimer at the time of creation.
*
* @return The Serializable object that was passed in at txtimer creation, or null if the
* info argument passed in at txtimer creation was null.
* @throws IllegalStateException If this method is invoked while the instance is in
* a state that does not allow access to this method.
* @throws javax.ejb.NoSuchObjectLocalException
* If invoked on a txtimer that has expired or has been cancelled.
* @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
*/
public Serializable getInfo() throws IllegalStateException, NoSuchObjectLocalException, EJBException
{
assertTimedOut();
assertAllowedOperation("Timer.getInfo");
return info;
}
/**
* Get a serializable handle to the txtimer. This handle can be used at a later time to
* re-obtain the txtimer reference.
*
* @return Handle of the Timer
* @throws IllegalStateException If this method is invoked while the instance is in
* a state that does not allow access to this method.
* @throws javax.ejb.NoSuchObjectLocalException
* If invoked on a txtimer that has expired or has been cancelled.
* @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
*/
public TimerHandle getHandle() throws IllegalStateException, NoSuchObjectLocalException, EJBException
{
assertTimedOut();
assertAllowedOperation("Timer.getHandle");
return new TimerHandleImpl(this);
}
/**
* Get the schedule expression corresponding to this timer.
* @return
* @throws IllegalStateException If this method is invoked while the instance
* is in a state that does not allow access to this method. Also thrown if
* invoked on a timer that was created with one of the non-ScheduleExpression
* TimerService.createTimer APIs.
* @throws NoSuchObjectLocalException If invoked on a timer that has expired or
* has been cancelled.
* @throws EJBException If this method could not complete due to a system-level
* failure.
* @since 3.1
*/
public ScheduleExpression getSchedule()
{
throw new UnsupportedOperationException("getSchedule: NOT IMPLEMENTED");
}
/**
* Query whether this timer is a calendar-based timer.
* @return true if this timer is a calendar-based timer.
* @throws IllegalStateException If this method is invoked while the instance
* is in a state that does not allow access to this method.
* @throws NoSuchObjectLocalException If invoked on a timer that has expired
* or has been cancelled.
* @throws EJBException If this method could not complete due to a system-level failure.
* @since 3.1
*/
public boolean isCalendarTimer()
{
throw new UnsupportedOperationException("isCalendarTimer: NOT IMPLEMENTED");
}
/**
* Query whether this timer has persistent semantics.
*
* @return true if this timer has persistent guarantees.
* @throws IllegalStateException If this method is invoked while the instance
* is in a state that does not allow access to this method.
* @throws NoSuchObjectLocalException If invoked on a timer that has expired
* or has been cancelled.
* @throws EJBException If this method could not complete due to a system-level failure.
* @since 3.1
*/
public boolean isPersistent()
{
throw new UnsupportedOperationException("isPersistent: NOT IMPLEMENTED");
}
/**
* Return true if objectId, createDate, periode are equal
*/
public boolean equals(Object obj)
{
if (obj == this) return true;
if (obj instanceof TimerImpl)
{
TimerImpl other = (TimerImpl)obj;
return hashCode() == other.hashCode();
}
return false;
}
/**
* Hash code based on the Timers invariant properties
*/
public int hashCode()
{
if (hashCode == 0)
{
String hash = "[" + timerId + "," + timedObjectId + "," + firstTime + "," + periode + "]";
hashCode = hash.hashCode();
}
return hashCode;
}
/**
* Returns a string representation of the object.
*/
public String toString()
{
long remaining = nextExpire - System.currentTimeMillis();
String retStr = "[id=" + timerId + ",target=" + timedObjectId + ",remaining=" + remaining + ",periode=" + periode +
"," + TIMER_STATES[timerState] + "]";
return retStr;
}
/**
* Register the txtimer with the current transaction
*/
private void registerTimerWithTx()
{
Transaction tx = timerService.getTransaction();
if (tx != null)
{
try
{
tx.registerSynchronization(this);
}
catch (Exception e)
{
log.error("Cannot register txtimer with Tx: " + this);
}
}
}
private void setTimerState(int state)
{
log.debug("setTimerState: " + TIMER_STATES[state]);
timerState = state;
}
private void startInTx()
{
// JBAS-4330, provide a meaningful name to the timer thread, needs jdk5+
utilTimer = new Timer("EJB-Timer-" + timerId + timedObjectId);
if (timerService.getTransaction() != null)
{
// don't schedule the timeout yet
setTimerState(STARTED_IN_TX);
}
else
{
setTimerState(ACTIVE);
scheduleTimeout();
}
}
private void cancelInTx()
{
if (timerService.getTransaction() != null)
setTimerState(CANCELED_IN_TX);
else
killTimer();
}
private void scheduleTimeout()
{
if (periode > 0)
utilTimer.schedule(new TimerTaskImpl(this), new Date(nextExpire), periode);
else
utilTimer.schedule(new TimerTaskImpl(this), new Date(nextExpire));
}
/**
* Throws NoSuchObjectLocalException if the txtimer was canceled or has expired
*/
private void assertTimedOut()
{
if (timerState == EXPIRED)
throw new NoSuchObjectLocalException("Timer has expired");
if (timerState == CANCELED_IN_TX || timerState == CANCELED)
throw new NoSuchObjectLocalException("Timer was canceled");
}
/**
* Throws an IllegalStateException if the Timer method call is not allowed in the current context
*/
private void assertAllowedOperation(String timerMethod)
{
AllowedOperationsAssociation.assertAllowedIn(timerMethod,
AllowedOperationsAssociation.IN_BUSINESS_METHOD |
AllowedOperationsAssociation.IN_EJB_TIMEOUT |
AllowedOperationsAssociation.IN_SERVICE_ENDPOINT_METHOD |
AllowedOperationsAssociation.IN_AFTER_BEGIN |
AllowedOperationsAssociation.IN_BEFORE_COMPLETION |
AllowedOperationsAssociation.IN_EJB_POST_CREATE |
AllowedOperationsAssociation.IN_EJB_REMOVE |
AllowedOperationsAssociation.IN_EJB_LOAD |
AllowedOperationsAssociation.IN_EJB_STORE);
}
// Synchronization **************************************************************************************************
/**
* This method is invoked before the start of the commit or rollback
* process. The method invocation is done in the context of the
* transaction that is about to be committed or rolled back.
*/
public void beforeCompletion()
{
switch(timerState)
{
case CANCELED_IN_TX:
timerService.removeTimer(this);
break;
case IN_TIMEOUT:
case RETRY_TIMEOUT:
if(periode == 0)
{
timerService.removeTimer(this);
}
break;
}
}
/**
* This method is invoked after the transaction has committed or
* rolled back.
*
* @param status The status of the completed transaction.
*/
public void afterCompletion(int status)
{
if (status == Status.STATUS_COMMITTED)
{
log.debug("commit: " + this);
switch (timerState)
{
case STARTED_IN_TX:
scheduleTimeout();
setTimerState(ACTIVE);
break;
case CANCELED_IN_TX:
cancelTimer();
break;
case IN_TIMEOUT:
case RETRY_TIMEOUT:
if(periode == 0)
{
setTimerState(EXPIRED);
cancelTimer();
}
else
{
setTimerState(ACTIVE);
}
break;
}
}
else if (status == Status.STATUS_ROLLEDBACK)
{
log.debug("rollback: " + this);
switch (timerState)
{
case STARTED_IN_TX:
cancelTimer();
break;
case CANCELED_IN_TX:
setTimerState(ACTIVE);
break;
case IN_TIMEOUT:
setTimerState(RETRY_TIMEOUT);
log.debug("retry: " + this);
timerService.retryTimeout(this);
break;
case RETRY_TIMEOUT:
if (periode == 0)
{
setTimerState(EXPIRED);
cancelTimer();
}
else
{
setTimerState(ACTIVE);
}
break;
}
}
}
// TimerTask ********************************************************************************************************
/**
* The TimerTask's run method is invoked by the java.util.Timer
*/
private class TimerTaskImpl extends TimerTask
{
private TimerImpl timer;
public TimerTaskImpl(TimerImpl timer)
{
this.timer = timer;
}
/**
* The action to be performed by this txtimer task.
*/
public void run()
{
log.debug("run: " + timer);
// Set next scheduled execution attempt. This is used only
// for reporting (getTimeRemaining()/getNextTimeout())
// and not from the underlying jdk timer implementation.
if (isActive() && periode > 0)
{
nextExpire += periode;
}
// If a retry thread is in progress, we don't want to allow another
// interval to execute until the retry is complete. See JIRA-1926.
if (isInRetry())
{
log.debug("Timer in retry mode, skipping this scheduled execution");
return;
}
if (isActive())
{
try
{
setTimerState(IN_TIMEOUT);
timedObjectInvoker.callTimeout(timer);
}
catch (Exception e)
{
log.error("Error invoking ejbTimeout", e);
}
finally
{
if (timerState == IN_TIMEOUT)
{
log.debug("Timer was not registered with Tx, resetting state: " + timer);
if (periode == 0)
{
setTimerState(EXPIRED);
killTimer();
}
else
{
setTimerState(ACTIVE);
}
}
}
}
}
}
}