Source code: org/mobicents/slee/runtime/facilities/TimerFacilityImpl.java
1 /***************************************************
2 * *
3 * Mobicents: The Open Source VoIP Platform *
4 * *
5 * Distributable under LGPL license. *
6 * See terms of license at gnu.org. *
7 * *
8 ***************************************************/
9
10 package org.mobicents.slee.runtime.facilities;
11
12
13 import java.io.Serializable;
14 import java.util.Date;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Timer;
18 import java.util.TimerTask;
19
20 import javax.slee.ActivityContextInterface;
21 import javax.slee.Address;
22 import javax.slee.EventTypeID;
23 import javax.slee.InvalidStateException;
24 import javax.slee.TransactionRolledbackLocalException;
25 import javax.slee.facilities.FacilityException;
26 import javax.slee.facilities.TimerEvent;
27 import javax.slee.facilities.TimerFacility;
28 import javax.slee.facilities.TimerID;
29 import javax.slee.facilities.TimerOptions;
30 import javax.slee.facilities.TimerPreserveMissed;
31 import javax.transaction.SystemException;
32
33 import org.jboss.logging.Logger;
34 import org.mobicents.slee.container.SleeContainer;
35 import org.mobicents.slee.container.management.ComponentKey;
36 import org.mobicents.slee.container.management.EventTypeIDImpl;
37 import org.mobicents.slee.runtime.ActivityContext;
38 import org.mobicents.slee.runtime.ActivityContextIDInterface;
39 import org.mobicents.slee.runtime.transaction.SleeTransactionManager;
40 import org.mobicents.slee.runtime.transaction.TransactionManagerImpl;
41 import org.mobicents.slee.runtime.transaction.TransactionalAction;
42
43
44
45 /**
46 * Implementation of the SLEE timer facility.
47 * timer is the timer object currently being examined.
48 * timer.scheduleTime is time that the Timer Event is scheduled to fire.
49 * timer.timeout is timeout for this timer (from TimerOptions).
50 * timer.numRepetitions is the total repetitions for this timer, 0 if infinite,
51 * 1 if non-periodic.
52 * timer.remainingRepetiton is the remaining repetition count, initially Long.MAX_VALUE
53 * for infinite periodic timers, timer.numRepetitions
54 * otherwise.
55 * timer.period is the timer period (Long.MAX_VALUE if non-periodic).
56 * timer.missed is the counter of undelivered late events.
57 *
58 * @author Tim - major refactoring - prevented thrashing, made threadsafe, added transaction checking
59 *
60 * @author M. Ranganathan
61 *
62 */
63 public class TimerFacilityImpl implements TimerFacility {
64
65 private static final int DEFAULT_TIMEOUT = 100;
66 public static final String JNDI_NAME = "timer";
67 private static final String FQN_PREFIX = "/facilities/timer/";
68 private static final String FQN_TIMERS_PREFIX = FQN_PREFIX + "timers/";
69 private static String tcache = TransactionManagerImpl.TCACHE;
70
71 // this is supposed to be the timer resolution in ms of the hosting OS/hardware
72 private int timerResolution = 10;
73
74 private Timer timer = new Timer();
75
76 private SleeContainer serviceContainer;
77
78 protected EventTypeIDImpl timerEventID;
79
80 protected ComponentKey timerEventKey;
81
82 private static Logger logger = Logger.getLogger(TimerFacilityImpl.class);
83
84
85
86 class TimerEventImpl implements TimerEvent {
87
88 private TimerID timerId;
89 private long scheduledTime;
90 private long expiryTime;
91 private long period;
92 private int numRepetitions;
93 private int remainingRepetitions;
94 private int missedRepetitions;
95 //private ActivityContext ac;
96 //private String activityContextId;
97 private EventTypeID eventTypeID;
98 //private Address address;
99
100 TimerEventImpl(TimerID timerId, long scheduledTime, long expiryTime, long period,
101 int numRepetitions, int remainingRepetitions, int missedRepetitions,
102 EventTypeID eventTypeID) {
103 this.timerId = timerId;
104 this.scheduledTime = scheduledTime;
105 this.expiryTime = expiryTime;
106 this.period = period;
107 this.numRepetitions = numRepetitions;
108 this.remainingRepetitions = remainingRepetitions;
109 this.missedRepetitions = missedRepetitions;
110 //this.ac = ac;
111 //this.activityContextId = activityContextId;
112 this.eventTypeID = eventTypeID;
113 //this.address = address;
114 }
115
116 public TimerID getTimerID() { return this.timerId; }
117 public long getScheduledTime() { return this.scheduledTime; }
118 public long getExpiryTime() { return this.expiryTime; }
119 public long getPeriod() { return this.period; }
120 public int getNumRepetitions() { return this.numRepetitions; }
121 public int getRemainingRepetitions() { return this.remainingRepetitions; }
122 public int getMissedRepetitions() { return this.missedRepetitions; }
123 public EventTypeID getEventTypeID() { return this.eventTypeID; }
124 //public Object getEventObject() { return null; }
125 //public Address getAddress() { return this.address; }
126
127 /*
128 public ActivityContext getActivityContext() {
129 ActivityContext ac =
130 TimerFacilityImpl.this.serviceContainer.getActivityContextFactory().getActivityContextByKey(this.activityContextId);
131 if (ac == null) logger.error("Can't find ac in cache!");
132 return ac;
133 }
134 */
135
136 /*
137 public String getActivityContextID() {
138 return getActivityContext().getActivityContextId();
139 }
140 */
141 }
142
143 class MyTimerTask extends TimerTask implements Serializable {
144
145 private TimerID timerId;
146 private String activityContextId;
147 private Address address;
148 private TimerOptions timerOptions;
149 long startTime;
150 int numRepetitions;
151 int remainingRepetitions;
152 int missedRepetitions;
153 long period;
154
155 public MyTimerTask(TimerID timerId, String activityContextId, Address address,
156 long startTime, long period, int numRepetitions, TimerOptions timerOptions) {
157 this.timerId = timerId;
158 //this.aci = aci;
159 this.activityContextId = activityContextId;
160 this.address = address;
161 this.startTime = startTime;
162 this.period = period;
163 this.numRepetitions = numRepetitions;
164
165 // for infinitely repetitive events the remainingRepetitions value has to be always Int.MAX_VALUE
166 if (numRepetitions == 0) this. remainingRepetitions = Integer.MAX_VALUE;
167 else this.remainingRepetitions = numRepetitions;
168
169 this.missedRepetitions = 0;
170 this.timerOptions = timerOptions;
171 }
172
173 TimerID getTimerID() { return timerId; }
174
175 /*
176 * This is where it all happens
177 *
178 * see SLEE spec 1.0 Sec. 13.1.6
179 *
180 * I know it doesn't look the same but this follows the pseudo code in 13.1.6!
181 * It's just simplified since the java.util.Timer class takes care of the scheduling stuff.
182 * This allows us to avoid having to wake up every x milliseconds and check our Timers
183 * to see if there's anything to fire, even when there's nothing to do.
184 * If we use the java.util.Timer class with scheduleAtFixedRate then it
185 * optimise the scheduling to avoid unnecessary polling.
186 *
187 * @see java.lang.Runnable#run()
188 */
189 public void run() {
190 //TODO PERSISTENT TIMERS!!
191
192 logger.debug("In MyTimerTask.run()");
193
194 long tRes = timerResolution;
195 long tSys = System.currentTimeMillis();
196 long tDto = DEFAULT_TIMEOUT;
197 long tFire = tSys + tRes;
198
199 boolean postIt = false;
200
201 if (this.timerOptions.getPreserveMissed() == TimerPreserveMissed.ALL) {
202 /* Always post the event. Remember, this method will get called for late events since
203 * this TimerTask was scheduled to run at fixed rate. see Timer.scheduleAtFixedRate()
204 */
205 postIt = true;
206 logger.debug("TimerPreserveMissed.ALL so posting the event");
207 } else {
208 long timeOut;
209 if (this.timerOptions.getTimeout() == 0) {
210 timeOut = tDto;
211 } else {
212 timeOut = this.timerOptions.getTimeout();
213 }
214 timeOut = Math.min(Math.max(timeOut, tRes), this.period);
215 logger.debug("I'm using " + timeOut + " for the timeout to work out whether the event's late");
216
217 if (this.timerOptions.getPreserveMissed() == TimerPreserveMissed.NONE) {
218 //If events are late we NEVER want to post them
219 logger.debug("TimerPreserveMissed.NONE");
220 if (tSys <= this.scheduledExecutionTime() + timeOut) {
221 //Event is not late
222 postIt = true;
223 logger.debug("Event is NOT late so I'm posting it");
224 } else {
225 //Event is late so NOT posting it
226 logger.debug("Event is late so I'm NOT posting it");
227 this.missedRepetitions++;
228 }
229 } else if (this.timerOptions.getPreserveMissed() == TimerPreserveMissed.LAST) {
230 //Count missed events.
231 //Preserve the last missed event
232 logger.debug("TimerPreserveMissed.LAST");
233
234 if (remainingRepetitions > 1 &&
235 (tSys > this.scheduledExecutionTime() + timeOut)) {
236 //Event is not the last one and event is late
237 logger.debug("Event is late and NOT the last one so I'm NOT posting it");
238 this.missedRepetitions++;
239 }
240 else {
241 logger.debug("Event is either NOT late, or late and is the last event so I'm posting it");
242 postIt = true;
243 }
244 }
245 }
246
247 logger.debug("SCHEDULED EXECUTION TIME IS " + this.scheduledExecutionTime());
248 logger.debug("Remaining repetitions:" + this.remainingRepetitions);
249
250 decrementRemainingRepetitions();
251
252 if (postIt) {
253 //Post the event
254 TimerEventImpl timerEvent =
255 new TimerEventImpl(this.timerId, this.scheduledExecutionTime(), tSys, this.period,
256 this.numRepetitions, this.remainingRepetitions, this.missedRepetitions,
257 timerEventID);
258 this.missedRepetitions = 0;
259 postEvent(timerEvent);
260 }
261
262 if (remainingRepetitions == 0) {
263 //Remove reference to the Timer so the ActivityContext can be reclaimed Spec 13.1.2.1
264 logger.debug("Timer has expired - removing it");
265 cancelTimer(timerId);
266 }
267 }
268
269 /**
270 * Decrement remainingRepetitions by 1 if this is not an infinitely repeatable timer
271 */
272 private void decrementRemainingRepetitions() {
273 if (numRepetitions > 0) this.remainingRepetitions--;
274 }
275
276 void postEvent(TimerEventImpl timerEvent) {
277 logger.debug("Posting timer event:" + timerEvent);
278 //Post the timer event to the queue.
279 try {
280
281 Object act = serviceContainer.getActivityContextFactory().getActivityFromKey(this.activityContextId);
282
283 serviceContainer.getSleeEndpoint().enqueueEvent(timerEventID, timerEvent,
284 act, this.address);
285
286
287
288 } catch(InvalidStateException e) {
289 logger.error("Failed to send timer event", e);
290 }
291 }
292
293 /**
294 * @return the activity context interface for the timer task.
295 */
296 public String getActivityContextId() {
297 // TODO Auto-generated method stub
298 return this.activityContextId;
299 }
300 }
301
302
303 class TimerFacilityAction implements TransactionalAction {
304
305 private static final int TYPE_SET_ONETIME = 0;
306 private static final int TYPE_SET_PERIOD = 1;
307 private static final int TYPE_CANCEL = 2;
308
309 private TimerTask task;
310 private Date startTime;
311 private long period;
312 private TimerID timerID;
313
314 private int actionType; // Saves having to define a new class when you have several actions
315
316 public String toString() {
317 return this.getClass() + " Type: " + actionType;
318 }
319
320 TimerFacilityAction(MyTimerTask task, Date startTime, long period) {
321 this(task, startTime);
322 this.period = period;
323 actionType = TYPE_SET_PERIOD;
324
325 this.timerID = task.getTimerID();
326 }
327
328 TimerFacilityAction(MyTimerTask task, Date startTime) {
329 this.task = task;
330 this.startTime = startTime;
331 actionType = TYPE_SET_ONETIME;
332
333 this.timerID = task.getTimerID();
334 }
335
336 TimerFacilityAction(MyTimerTask task) {
337 this.task = task;
338 actionType = TYPE_CANCEL;
339 }
340
341 public void execute() {
342 TimerFacilityImpl tf = TimerFacilityImpl.this;
343 if (actionType == TYPE_SET_ONETIME) {
344 logger.debug("===Scheduling one-time timer");
345 tf.timer.schedule(task, startTime);
346 } else if (actionType == TYPE_SET_PERIOD){
347 tf.timer.scheduleAtFixedRate(task, startTime, period);
348 logger.debug("===Scheduling periodic timer");
349 } else if (actionType == TYPE_CANCEL) {
350 logger.debug("===Cancelling timer");
351 this.task.cancel();
352 }
353 }
354 }
355
356 public TimerFacilityImpl(SleeContainer serviceContainer) {
357 this.serviceContainer = serviceContainer;
358 this.timerEventKey = new ComponentKey("javax.slee.facilities.TimerEvent", "javax.slee", "1.0");
359 this.timerEventID = this.serviceContainer.getEventType(timerEventKey);
360 }
361
362 /* One shot timer
363 * @see javax.slee.facilities.TimerFacility#setTimer(javax.slee.ActivityContextInterface, javax.slee.Address, long, javax.slee.facilities.TimerOptions)
364 */
365 public TimerID setTimer(ActivityContextInterface aci, Address address,
366 long startTime, TimerOptions timerOptions) throws NullPointerException,
367 IllegalArgumentException, FacilityException {
368
369 if (aci == null ) throw new NullPointerException("Null ActivityContextInterface");
370 if (timerOptions == null) throw new NullPointerException("Null TimerOptions");
371 if (startTime < 0) throw new IllegalArgumentException("startTime < 0");
372 logger.debug("setTimer " + ((ActivityContextIDInterface)aci).retrieveActivityContextID());
373
374 SleeTransactionManager txMgr = SleeContainer.getTransactionManager();
375
376 boolean startedTx = txMgr.requireTransaction();
377
378 TimerIDImpl timerID = new TimerIDImpl();
379 logger.debug("Timer id is: " + timerID);
380
381 long now = System.currentTimeMillis();
382 if (startTime < now) startTime = now;
383
384 MyTimerTask task = new MyTimerTask(timerID,
385 ((ActivityContextIDInterface)aci).retrieveActivityContextID(),
386 address, startTime,
387 Long.MAX_VALUE, 1, timerOptions);
388
389 /* put it into the treecache in it's own node since we do not want to lock all the timers
390 * we add the whole object in, rather than the attributes since we're not interested
391 * in being able to rollback changes to the timer
392 */
393 try {
394 txMgr.putObject(tcache,FQN_TIMERS_PREFIX + timerID.toString(), "task", task);
395 } catch (SystemException e) {
396 throw new FacilityException("Failed to add timer to cache", e);
397 }
398 logger.debug("Added new timer to cache");
399
400 //Attach to activity context
401 ((ActivityContextIDInterface)aci).retrieveActivityContext().attachTimer(timerID);
402
403 //Create an action that actually schedules the timer - we execute this on commit of the tx
404 TimerFacilityAction action = new TimerFacilityAction(task, new Date(startTime));
405
406 try {
407 SleeContainer.getTransactionManager().addAfterCommitAction(action);
408 } catch (SystemException e) {
409 //This only happens if it is not in a transaction - this should never happen
410 logger.error(e);
411 }
412
413 //addToMaps(timerId, task);
414
415 //If we started a tx for this operation, we commit it now
416 if (startedTx) {
417 try {
418 SleeContainer.getTransactionManager().commit();
419 } catch(Exception e) {
420 throw new TransactionRolledbackLocalException("Failed to commit transaction");
421 }
422 }
423
424 return timerID;
425 }
426
427
428
429 /* Periodic timer
430 * @see javax.slee.facilities.TimerFacility#setTimer(javax.slee.ActivityContextInterface, javax.slee.Address, long, long, int, javax.slee.facilities.TimerOptions)
431 */
432 public TimerID setTimer(ActivityContextInterface aci, Address address,
433 long startTime, long period, int numRepetitions, TimerOptions timerOptions)
434 throws NullPointerException, IllegalArgumentException,
435 TransactionRolledbackLocalException, FacilityException {
436
437 if (aci == null ) throw new NullPointerException("Null ActivityContextInterface");
438 if (timerOptions == null) throw new NullPointerException("Null TimerOptions");
439 if (startTime < 0) throw new IllegalArgumentException("startTime < 0");
440 if (period <= 0) throw new IllegalArgumentException("period <= 0");
441 if (numRepetitions < 0) throw new IllegalArgumentException("numRepetitions < 0");
442 if (timerOptions.getTimeout() > period) throw new IllegalArgumentException("timeout > period"); //SPEC 13.1.3
443 if (timerOptions.getTimeout() < this.getResolution())
444 timerOptions.setTimeout(Math.min(period, this.getResolution())); //SPEC 13.1.3
445
446 SleeTransactionManager txMgr = SleeContainer.getTransactionManager();
447
448 boolean startedTx = txMgr.requireTransaction();
449
450
451 logger.debug("setTimer: startTime = " + startTime + " period = " +
452 period + " numRepetitions = " + numRepetitions + " timeroptions =" + timerOptions );
453
454 TimerIDImpl timerID = new TimerIDImpl();
455 logger.debug("Timer id is: " + timerID);
456
457 long now = System.currentTimeMillis();
458 if (startTime < now) startTime = now;
459
460 logger.debug("START TIME IS " + startTime);
461
462 MyTimerTask task = new MyTimerTask(timerID,
463 ((ActivityContextIDInterface)aci).retrieveActivityContextID(),
464 address, startTime,
465 period, numRepetitions, timerOptions);
466
467 /* put it into the treecache in it's own node since we do not want to lock all the timers
468 * we add the whole object in, rather than the attributes since we're not interested
469 * in being able to rollback changes to the timer
470 */
471 try {
472 txMgr.putObject(tcache,FQN_TIMERS_PREFIX + timerID.toString(), "task", task);
473 } catch (SystemException e) {
474 throw new FacilityException("Failed to add timer to cache", e);
475 }
476 logger.debug("Added new timer to cache");
477
478 //Attach to activity context
479 ((ActivityContextIDInterface)aci).retrieveActivityContext().attachTimer(timerID);
480
481 // Create an action that actually schedules the timer - we execute this on commit of the tx
482 TimerFacilityAction action = new TimerFacilityAction(task, new Date(startTime), period);
483
484 try {
485 SleeContainer.getTransactionManager().addAfterCommitAction(action);
486 } catch (SystemException e) {
487 //This only happens if it is not in a transaction - this should never happen
488 logger.error(e);
489 }
490
491 //addToMaps(timerId, task);
492
493 //If we started a tx for this operation, we commit it now
494 if (startedTx) {
495 try {
496 SleeContainer.getTransactionManager().commit();
497 } catch(Exception e) {
498 throw new TransactionRolledbackLocalException("Failed to commit transaction");
499 }
500 }
501
502 return timerID;
503 }
504
505 /* (non-Javadoc)
506 * @see javax.slee.facilities.TimerFacility#cancelTimer(javax.slee.facilities.TimerID)
507 */
508 public synchronized void cancelTimer(TimerID timerID) throws NullPointerException,
509 TransactionRolledbackLocalException, FacilityException {
510
511 if (timerID == null ) throw new NullPointerException("Null TimerID");
512
513 SleeTransactionManager txMgr = SleeContainer.getTransactionManager();
514
515 boolean startedTx = txMgr.requireTransaction();
516
517 logger.debug("Started tx: " + startedTx);
518
519 logger.debug("Cancelling timer");
520
521 //Remove the timer from the tree cache
522 String fqn = FQN_TIMERS_PREFIX + timerID.toString();
523 logger.debug("Removing node: " + fqn);
524 MyTimerTask task = null;
525 try {
526
527 task = (MyTimerTask)txMgr.getObject(tcache,fqn, "task");
528 if (task == null) {
529 logger.debug("Can't find timer task in cache!");
530 return;
531 }
532
533 //Detach this timer from the ac
534 ActivityContext ac =
535 this.serviceContainer.getActivityContextFactory().getActivityContextByKey(task.getActivityContextId());
536 if (ac == null) throw new FacilityException("Can't find ac in cache!");
537
538 ac.detachTimer(timerID);
539
540 //Remove the node
541 txMgr.removeNode(tcache,fqn);
542 } catch (SystemException e) {
543 throw new FacilityException("Failed to remove timer from cache", e);
544 }
545
546
547
548 /* Hack
549 * It seems that, if there's a transactional action to set a timer to start
550 * immediately, and there's a cancel for that timer in the same transaction.
551 * Then, when the transaction commits, the timer should never start ticking, even
552 * thought the cancel action is executed after the setimer action
553 * (see TCK test 1170)
554 * This means, when we add a cancel timer action to the transaction, we only actually add
555 * it if the corresponding settimer method isn't in the transaction - otherwise we just remove
556 * the first settime
557 *
558 */
559 boolean needToAdd = true;
560
561 try {
562 List actions = ((TransactionManagerImpl)txMgr).getCommitActions();
563 if (actions != null) {
564 Iterator iter = actions.iterator();
565 logger.debug("There are " + actions.size() + " actions");
566 while (iter.hasNext()) {
567 TransactionalAction action = (TransactionalAction)iter.next();
568
569 if (action instanceof TimerFacilityAction) {
570 logger.debug("Timerfacilityaction");
571 TimerFacilityAction tfAction = (TimerFacilityAction)action;
572 logger.debug("Action 2 is: " + tfAction);
573 logger.debug("timerid is: " + tfAction.timerID);
574 logger.debug("actiontype is: " + tfAction.actionType);
575 logger.debug("timerid is: " + timerID);
576 if (((tfAction.actionType == TimerFacilityAction.TYPE_SET_ONETIME) ||
577 (tfAction.actionType == TimerFacilityAction.TYPE_SET_PERIOD)) &&
578 tfAction.timerID.equals(timerID))
579 {
580 logger.debug("Removing it:");
581 //Remove it
582 iter.remove();
583 needToAdd = false;
584
585 break;
586 }
587 }
588 }
589 }
590 } catch(SystemException e) {
591 throw new FacilityException("Failed to get actions", e);
592 }
593
594 logger.debug("Need to add: " + needToAdd);
595
596 //Add an action representing the cancelling of the timer task
597 if (needToAdd) {
598 TimerFacilityAction action = new TimerFacilityAction(task);
599 try {
600 SleeContainer.getTransactionManager().addAfterCommitAction(action);
601 logger.debug("Added cancel timer commit action");
602
603 } catch (SystemException e) {
604 //This only happens if it is not in a transaction - this should never happen
605 logger.error(e);
606 }
607 }
608
609 if (startedTx) {
610 try {
611 logger.debug("started tx so committing it");
612 SleeContainer.getTransactionManager().commit();
613 } catch(Exception e) {
614 throw new TransactionRolledbackLocalException("Failed to commit transaction");
615 }
616 }
617 }
618
619 /* (non-Javadoc)
620 * @see javax.slee.facilities.TimerFacility#getResolution()
621 */
622 public long getResolution() throws FacilityException {
623 return this.timerResolution;
624 }
625
626 /* (non-Javadoc)
627 * @see javax.slee.facilities.TimerFacility#getDefaultTimeout()
628 */
629 public long getDefaultTimeout() throws FacilityException {
630 return DEFAULT_TIMEOUT;
631 }
632
633 /** stop the timer. This is for JMX management interfaces
634 *
635 */
636 public void stop() {
637 this.timer.cancel();
638 }
639
640 /**
641 * Start the timer. This is for jmx management interfaces.
642 *
643 */
644 public void start() {
645 }
646
647 /**
648 * Set the timer resolution. This is for jmx interfaces.
649 *
650 * @throws Exception if the resolution is smaller than 10 milisecods
651 */
652 public void setTimerResolution(int resolution) {
653 if (resolution < 10 ) throw new IllegalArgumentException("min resolution is 10 miliseconds");
654 else this.timerResolution = resolution;
655 }
656
657 /**
658 * Get the timer resolution. This is for jmx interfaces.
659 *
660 */
661 public int getTimerResolution() {
662 return this.timerResolution;
663 }
664
665
666 }
667