001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.ejb.txtimer;
023:
024: // $Id: TimerImpl.java 62329 2007-04-13 13:51:10Z dimitris@jboss.org $
025:
026: import java.io.Serializable;
027: import java.util.Date;
028: import java.util.Timer;
029: import java.util.TimerTask;
030:
031: import javax.ejb.EJBException;
032: import javax.ejb.NoSuchObjectLocalException;
033: import javax.ejb.TimerHandle;
034: import javax.transaction.Status;
035: import javax.transaction.Synchronization;
036: import javax.transaction.Transaction;
037:
038: import org.jboss.ejb.AllowedOperationsAssociation;
039: import org.jboss.logging.Logger;
040:
041: /**
042: * An implementation of an EJB Timer.
043: *
044: * Internally it uses a java.util.Timer and maintains its state in
045: * a Tx manner.
046: *
047: * @author Thomas.Diesler@jboss.org
048: * @author Dimitris.Andreadis@jboss.org
049: * @version $Revision: 62329 $
050: * @since 07-Apr-2004
051: */
052: public class TimerImpl implements javax.ejb.Timer, Synchronization {
053: // logging support
054: private static Logger log = Logger.getLogger(TimerImpl.class);
055:
056: /**
057: * Timer states and their allowed transitions
058: * <p/>
059: * CREATED - on create
060: * CREATED -> STARTED_IN_TX - when strated with Tx
061: * CREATED -> ACTIVE - when started without Tx
062: * STARTED_IN_TX -> ACTIVE - on Tx commit
063: * STARTED_IN_TX -> CANCELED - on Tx rollback
064: * ACTIVE -> CANCELED_IN_TX - on cancel() with Tx
065: * ACTIVE -> CANCELED - on cancel() without Tx
066: * CANCELED_IN_TX -> CANCELED - on Tx commit
067: * CANCELED_IN_TX -> ACTIVE - on Tx rollback
068: * ACTIVE -> IN_TIMEOUT - on TimerTask run
069: * IN_TIMEOUT -> ACTIVE - on Tx commit if periode > 0
070: * IN_TIMEOUT -> EXPIRED -> on Tx commit if periode == 0
071: * IN_TIMEOUT -> RETRY_TIMEOUT -> on Tx rollback
072: * RETRY_TIMEOUT -> ACTIVE -> on Tx commit/rollback if periode > 0
073: * RETRY_TIMEOUT -> EXPIRED -> on Tx commit/rollback if periode == 0
074: */
075: private static final int CREATED = 0;
076: private static final int STARTED_IN_TX = 1;
077: private static final int ACTIVE = 2;
078: private static final int CANCELED_IN_TX = 3;
079: private static final int CANCELED = 4;
080: private static final int EXPIRED = 5;
081: private static final int IN_TIMEOUT = 6;
082: private static final int RETRY_TIMEOUT = 7;
083:
084: private static final String[] TIMER_STATES = { "created",
085: "started_in_tx", "active", "canceled_in_tx", "canceled",
086: "expired", "in_timeout", "retry_timeout" };
087:
088: // The initial txtimer properties
089: private TimerServiceImpl timerService;
090: private String timerId;
091: private TimedObjectId timedObjectId;
092: private TimedObjectInvoker timedObjectInvoker;
093: private Date firstTime;
094: private long periode;
095: private Serializable info;
096:
097: private long nextExpire;
098: private int timerState;
099: private Timer utilTimer;
100: private int hashCode;
101:
102: /**
103: * Schedules the txtimer for execution at the specified time with a specified periode.
104: */
105: TimerImpl(TimerServiceImpl timerService, String timerId,
106: TimedObjectId timedObjectId,
107: TimedObjectInvoker timedObjectInvoker, Serializable info) {
108: this .timerService = timerService;
109: this .timerId = timerId;
110: this .timedObjectId = timedObjectId;
111: this .timedObjectInvoker = timedObjectInvoker;
112: this .info = info;
113:
114: setTimerState(CREATED);
115: }
116:
117: void startTimer(Date firstTime, long periode) {
118: this .firstTime = firstTime;
119: this .nextExpire = firstTime.getTime();
120: this .periode = periode;
121:
122: timerService.addTimer(this );
123: registerTimerWithTx();
124:
125: // the timer will actually go ACTIVE on tx commit
126: startInTx();
127: }
128:
129: public String getTimerId() {
130: return timerId;
131: }
132:
133: public TimedObjectId getTimedObjectId() {
134: return timedObjectId;
135: }
136:
137: public Date getFirstTime() {
138: return firstTime;
139: }
140:
141: public long getPeriode() {
142: return periode;
143: }
144:
145: public long getNextExpire() {
146: return nextExpire;
147: }
148:
149: public Serializable getInfoInternal() {
150: return info;
151: }
152:
153: public boolean isActive() {
154: return !isCanceled() && !isExpired();
155: }
156:
157: public boolean isInRetry() {
158: return timerState == RETRY_TIMEOUT;
159: }
160:
161: public boolean isCanceled() {
162: return timerState == CANCELED_IN_TX || timerState == CANCELED;
163: }
164:
165: public boolean isExpired() {
166: return timerState == EXPIRED;
167: }
168:
169: /**
170: * Cause the txtimer and all its associated expiration notifications to be cancelled.
171: *
172: * @throws IllegalStateException If this method is invoked while the instance is in
173: * a state that does not allow access to this method.
174: * @throws javax.ejb.NoSuchObjectLocalException
175: * If invoked on a txtimer that has expired or has been cancelled.
176: * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
177: */
178: public void cancel() throws IllegalStateException,
179: NoSuchObjectLocalException, EJBException {
180: assertTimedOut();
181: assertAllowedOperation("Timer.cancel");
182: registerTimerWithTx();
183: cancelInTx();
184: }
185:
186: /**
187: * Kill the timer, and remove it from the timer service
188: */
189: public void killTimer() {
190: log.debug("killTimer: " + this );
191: if (timerState != EXPIRED)
192: setTimerState(CANCELED);
193: timerService.removeTimer(this );
194: utilTimer.cancel();
195: }
196:
197: /**
198: * killTimer w/o persistence work
199: */
200: private void cancelTimer() {
201: if (timerState != EXPIRED)
202: setTimerState(CANCELED);
203: utilTimer.cancel();
204: }
205:
206: /**
207: * Kill the timer, do not remove from timer service
208: */
209: public void stopTimer() {
210: log.debug("stopTimer: " + this );
211: if (timerState != EXPIRED)
212: setTimerState(CANCELED);
213: utilTimer.cancel();
214: }
215:
216: /**
217: * Get the number of milliseconds that will elapse before the next scheduled txtimer expiration.
218: *
219: * @return Number of milliseconds that will elapse before the next scheduled txtimer expiration.
220: * @throws IllegalStateException If this method is invoked while the instance is in
221: * a state that does not allow access to this method.
222: * @throws javax.ejb.NoSuchObjectLocalException
223: * If invoked on a txtimer that has expired or has been cancelled.
224: * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
225: */
226: public long getTimeRemaining() throws IllegalStateException,
227: NoSuchObjectLocalException, EJBException {
228: assertTimedOut();
229: assertAllowedOperation("Timer.getTimeRemaining");
230: return nextExpire - System.currentTimeMillis();
231: }
232:
233: /**
234: * Get the point in time at which the next txtimer expiration is scheduled to occur.
235: *
236: * @return Get the point in time at which the next txtimer expiration is scheduled to occur.
237: * @throws IllegalStateException If this method is invoked while the instance is in
238: * a state that does not allow access to this method.
239: * @throws javax.ejb.NoSuchObjectLocalException
240: * If invoked on a txtimer that has expired or has been cancelled.
241: * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
242: */
243: public Date getNextTimeout() throws IllegalStateException,
244: NoSuchObjectLocalException, EJBException {
245: assertTimedOut();
246: assertAllowedOperation("Timer.getNextTimeout");
247: return new Date(nextExpire);
248: }
249:
250: /**
251: * Get the information associated with the txtimer at the time of creation.
252: *
253: * @return The Serializable object that was passed in at txtimer creation, or null if the
254: * info argument passed in at txtimer creation was null.
255: * @throws IllegalStateException If this method is invoked while the instance is in
256: * a state that does not allow access to this method.
257: * @throws javax.ejb.NoSuchObjectLocalException
258: * If invoked on a txtimer that has expired or has been cancelled.
259: * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
260: */
261: public Serializable getInfo() throws IllegalStateException,
262: NoSuchObjectLocalException, EJBException {
263: assertTimedOut();
264: assertAllowedOperation("Timer.getInfo");
265: return info;
266: }
267:
268: /**
269: * Get a serializable handle to the txtimer. This handle can be used at a later time to
270: * re-obtain the txtimer reference.
271: *
272: * @return Handle of the Timer
273: * @throws IllegalStateException If this method is invoked while the instance is in
274: * a state that does not allow access to this method.
275: * @throws javax.ejb.NoSuchObjectLocalException
276: * If invoked on a txtimer that has expired or has been cancelled.
277: * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
278: */
279: public TimerHandle getHandle() throws IllegalStateException,
280: NoSuchObjectLocalException, EJBException {
281: assertTimedOut();
282: assertAllowedOperation("Timer.getHandle");
283: return new TimerHandleImpl(this );
284: }
285:
286: /**
287: * Return true if objectId, createDate, periode are equal
288: */
289: public boolean equals(Object obj) {
290: if (obj == this )
291: return true;
292: if (obj instanceof TimerImpl) {
293: TimerImpl other = (TimerImpl) obj;
294: return hashCode() == other.hashCode();
295: }
296: return false;
297: }
298:
299: /**
300: * Hash code based on the Timers invariant properties
301: */
302: public int hashCode() {
303: if (hashCode == 0) {
304: String hash = "[" + timerId + "," + timedObjectId + ","
305: + firstTime + "," + periode + "]";
306: hashCode = hash.hashCode();
307: }
308: return hashCode;
309: }
310:
311: /**
312: * Returns a string representation of the object.
313: */
314: public String toString() {
315: long remaining = nextExpire - System.currentTimeMillis();
316: String retStr = "[id=" + timerId + ",target=" + timedObjectId
317: + ",remaining=" + remaining + ",periode=" + periode
318: + "," + TIMER_STATES[timerState] + "]";
319: return retStr;
320: }
321:
322: /**
323: * Register the txtimer with the current transaction
324: */
325: private void registerTimerWithTx() {
326: Transaction tx = timerService.getTransaction();
327: if (tx != null) {
328: try {
329: tx.registerSynchronization(this );
330: } catch (Exception e) {
331: log.error("Cannot register txtimer with Tx: " + this );
332: }
333: }
334: }
335:
336: private void setTimerState(int state) {
337: log.debug("setTimerState: " + TIMER_STATES[state]);
338: timerState = state;
339: }
340:
341: private void startInTx() {
342: // JBAS-4330, provide a meaningful name to the timer thread, needs jdk5+
343: utilTimer = new Timer("EJB-Timer-" + timerId + timedObjectId);
344:
345: if (timerService.getTransaction() != null) {
346: // don't schedule the timeout yet
347: setTimerState(STARTED_IN_TX);
348: } else {
349: scheduleTimeout();
350: setTimerState(ACTIVE);
351: }
352: }
353:
354: private void cancelInTx() {
355: if (timerService.getTransaction() != null)
356: setTimerState(CANCELED_IN_TX);
357: else
358: killTimer();
359: }
360:
361: private void scheduleTimeout() {
362: if (periode > 0)
363: utilTimer.schedule(new TimerTaskImpl(this ), new Date(
364: nextExpire), periode);
365: else
366: utilTimer.schedule(new TimerTaskImpl(this ), new Date(
367: nextExpire));
368: }
369:
370: /**
371: * Throws NoSuchObjectLocalException if the txtimer was canceled or has expired
372: */
373: private void assertTimedOut() {
374: if (timerState == EXPIRED)
375: throw new NoSuchObjectLocalException("Timer has expired");
376: if (timerState == CANCELED_IN_TX || timerState == CANCELED)
377: throw new NoSuchObjectLocalException("Timer was canceled");
378: }
379:
380: /**
381: * Throws an IllegalStateException if the Timer method call is not allowed in the current context
382: */
383: private void assertAllowedOperation(String timerMethod) {
384: AllowedOperationsAssociation
385: .assertAllowedIn(
386: timerMethod,
387: AllowedOperationsAssociation.IN_BUSINESS_METHOD
388: | AllowedOperationsAssociation.IN_EJB_TIMEOUT
389: | AllowedOperationsAssociation.IN_SERVICE_ENDPOINT_METHOD
390: | AllowedOperationsAssociation.IN_AFTER_BEGIN
391: | AllowedOperationsAssociation.IN_BEFORE_COMPLETION
392: | AllowedOperationsAssociation.IN_EJB_POST_CREATE
393: | AllowedOperationsAssociation.IN_EJB_REMOVE
394: | AllowedOperationsAssociation.IN_EJB_LOAD
395: | AllowedOperationsAssociation.IN_EJB_STORE);
396: }
397:
398: // Synchronization **************************************************************************************************
399:
400: /**
401: * This method is invoked before the start of the commit or rollback
402: * process. The method invocation is done in the context of the
403: * transaction that is about to be committed or rolled back.
404: */
405: public void beforeCompletion() {
406: switch (timerState) {
407: case CANCELED_IN_TX:
408: timerService.removeTimer(this );
409: break;
410:
411: case IN_TIMEOUT:
412: case RETRY_TIMEOUT:
413: if (periode == 0) {
414: timerService.removeTimer(this );
415: }
416: break;
417: }
418: }
419:
420: /**
421: * This method is invoked after the transaction has committed or
422: * rolled back.
423: *
424: * @param status The status of the completed transaction.
425: */
426: public void afterCompletion(int status) {
427: if (status == Status.STATUS_COMMITTED) {
428: log.debug("commit: " + this );
429:
430: switch (timerState) {
431: case STARTED_IN_TX:
432: scheduleTimeout();
433: setTimerState(ACTIVE);
434: break;
435:
436: case CANCELED_IN_TX:
437: cancelTimer();
438: break;
439:
440: case IN_TIMEOUT:
441: case RETRY_TIMEOUT:
442: if (periode == 0) {
443: setTimerState(EXPIRED);
444: cancelTimer();
445: } else {
446: setTimerState(ACTIVE);
447: }
448: break;
449: }
450: } else if (status == Status.STATUS_ROLLEDBACK) {
451: log.debug("rollback: " + this );
452:
453: switch (timerState) {
454: case STARTED_IN_TX:
455: cancelTimer();
456: break;
457:
458: case CANCELED_IN_TX:
459: setTimerState(ACTIVE);
460: break;
461:
462: case IN_TIMEOUT:
463: setTimerState(RETRY_TIMEOUT);
464: log.debug("retry: " + this );
465: timerService.retryTimeout(this );
466: break;
467:
468: case RETRY_TIMEOUT:
469: if (periode == 0) {
470: setTimerState(EXPIRED);
471: cancelTimer();
472: } else {
473: setTimerState(ACTIVE);
474: }
475: break;
476: }
477: }
478: }
479:
480: // TimerTask ********************************************************************************************************
481:
482: /**
483: * The TimerTask's run method is invoked by the java.util.Timer
484: */
485: private class TimerTaskImpl extends TimerTask {
486: private TimerImpl timer;
487:
488: public TimerTaskImpl(TimerImpl timer) {
489: this .timer = timer;
490: }
491:
492: /**
493: * The action to be performed by this txtimer task.
494: */
495: public void run() {
496: log.debug("run: " + timer);
497:
498: // Set next scheduled execution attempt. This is used only
499: // for reporting (getTimeRemaining()/getNextTimeout())
500: // and not from the underlying jdk timer implementation.
501: if (isActive() && periode > 0) {
502: nextExpire += periode;
503: }
504:
505: // If a retry thread is in progress, we don't want to allow another
506: // interval to execute until the retry is complete. See JIRA-1926.
507: if (isInRetry()) {
508: log
509: .debug("Timer in retry mode, skipping this scheduled execution");
510: return;
511: }
512:
513: if (isActive()) {
514: try {
515: setTimerState(IN_TIMEOUT);
516: timedObjectInvoker.callTimeout(timer);
517: } catch (Exception e) {
518: log.error("Error invoking ejbTimeout: "
519: + e.toString());
520: } finally {
521: if (timerState == IN_TIMEOUT) {
522: log
523: .debug("Timer was not registered with Tx, resetting state: "
524: + timer);
525: if (periode == 0) {
526: setTimerState(EXPIRED);
527: killTimer();
528: } else {
529: setTimerState(ACTIVE);
530: }
531: }
532: }
533: }
534: }
535: }
536: }
|