Sunday, June 5, 2011

EJB 3 example - How to use Timer service (JBoss AS 6)

Through this post, I wanna show you how easy it is to use the timer service provided by EJB 3 compliant application server. The timer service is a very convenient feature that helps you schedule tasks that must be ran once or repeatedly. Besides, timers created with the timer service are persisted so that they're kept running when the server is restarted (useful for crash-recovery). As stated in the title, I use JBoss AS 6 to deploy my example but you could deploy yours on any other EJB 3 compliant application server.

Description of my example

In my example, I've implemented a stateless session bean that manage user subscriptions to a website. At each subscription, I'll start a timer that will check, every once in a while, whether the subscribed user is still active (for that purpose, every user record in my database will contain a column that contains the last connection date).

Java EE timer service : annotation or deployment descriptor?

As almost every service available in Java EE, you could configure timer service either with annotations or with the deployment descriptor. But you'll typically configure it with annotation because configuring it via the description descriptor will make you loose some flexibility : using annotation, you'll be able to start, cancel, define timer delay, and so on, from within your code.

Where to use timer service?

As for EJB 3 specifications, timer can be created only for stateless session bean and for message-driven bean.

How to use timer service ?

1° In order to create a timer, you'll need to get hold of an instance of TimerService. To do so, there are 3 possibilities : 
  • through dependency injection :  @resource private TimerService timerService;
  • through JNDI lookup
  • through EJBContext : @resource private EJBContext context; context.getTimerService( );
2° Specify the method within your bean, that will be executed by the timer. To do so, there are 2 possibilities
  • Implement a method with the following signature : public/protected void methodName (Timer timer) and annotate it with @Timeout.
  • Have your bean implement the TimedObject interface. You'll  then have to implement the public void ejbTimeout(Timer timer) method
3° Finally, you'll have to create a timer from within the bean. To do so, you can call methods such as createTimer(...) and createCalendarTimer(...) on the TimerServiceInstance.

Code example
package beans.userManagement;

import java.io.Serializable;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Date;

import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

import tools.StringManager;
import beans.dao.UserDao;
import beans.emailManagement.EmailManagement;
import dto.User;
import exceptions.EmailException;
import exceptions.UserManagementException;

/**
 * Session Bean implementation class UserSubscriptionManagementBean
 */
@Stateless
public class UserSubscriptionManagementBean implements UserSubscriptionManagementLocal, UserSubscriptionManagementRemote{
    private static final long NB_DAYS_BEFORE_REMOVAL = 30L;
    private static final long NB_DAYS_BEFORE_WARNING = 15L;
    private static final long MAXIMUM_INACTIVE_PERIOD_BEFORE_REMOVAL = 1000L * 3600L * 24L * NB_DAYS_BEFORE_REMOVAL; // maximum inactivity period = 30 days 
    private static final long MAXIMUM_INACTIVE_PERIOD_BEFORE_WARNING = 1000L * 3600L * 24L * NB_DAYS_BEFORE_WARNING;

    @EJB
    private UserDao userDao;
    @EJB
    private EmailManagement emailManager;
    @Resource
    private TimerService timerService;

    private User user;

    /**
     * Default constructor. 
     */
    public UserSubscriptionManagementBean() {}

    @Override
    public void subscribe(String email, String nickname, String password, 
            String firstName, String lastName, Date birthdate, Date subscriptionDate) throws UserManagementException {

        // insert new User in database
        ...
        
        userDao.insert(user);
        // starting a timer to cleanup inactive user / send warning
        createTimers();
    }

    private void createTimers(){
        timerService.createTimer(MAXIMUM_INACTIVE_PERIOD_BEFORE_WARNING, user.getEmail());
        timerService.createTimer(MAXIMUM_INACTIVE_PERIOD_BEFORE_REMOVAL, user);
    }

    @Timeout
    public void cleanupInactiveUsers(Timer timer){
        Serializable info = timer.getInfo();

        if(info != null){
            if(info instanceof User){ // deleting inactive user
                User userToCheck = userDao.find(((User)info).getEmail());

                if(userToCheck == null){
                    return;
                }
                
                Long inactivityPeriod = System.currentTimeMillis() - userToCheck.getLastConnectionDate().getTime();

                if(inactivityPeriod >= MAXIMUM_INACTIVE_PERIOD_BEFORE_REMOVAL){
                    userDao.delete(userToCheck);
                    StringBuilder object = new StringBuilder("Automatic unsubscription");
                    StringBuilder message = new StringBuilder();
                    message.append("Your Crowd Freighting account has been deleted due to " + NB_DAYS_BEFORE_REMOVAL + " days inactivity");
                    message.append("\n\nKind regards, \nThe Crowd Freighting Crew.");

                    try {
                        emailManager.sendEmail(new String[]{userToCheck.getEmail()}, 
                                               null, 
                                               null, 
                                               object.toString(), 
                                               message.toString());                    
                    } catch (EmailException e) {
                        e.printStackTrace();
                    }
                }else{
                    timerService.createTimer(MAXIMUM_INACTIVE_PERIOD_BEFORE_REMOVAL - inactivityPeriod, userToCheck);
                }
            }else if(info instanceof String){ // send warning to user
                User userToCheck = userDao.find((String) info);
                Long inactivityPeriod = System.currentTimeMillis() - userToCheck.getLastConnectionDate().getTime();

                if(inactivityPeriod >= MAXIMUM_INACTIVE_PERIOD_BEFORE_WARNING){
                    StringBuilder object = new StringBuilder("Subscription warning");
                    StringBuilder message = new StringBuilder();
                    message.append("Your Crowd Freighting account has been inactive for " + NB_DAYS_BEFORE_WARNING + " days.");
                    message.append("\nIt will be removed from our database when it reached " + NB_DAYS_BEFORE_REMOVAL + " days of inactvity");
                    message.append("\n\nKind regards, \nThe Crowd Freighting Crew.");
                    
                    try {
                        emailManager.sendEmail(new String[]{userToCheck.getEmail()}, 
                                               null, 
                                               null, 
                                               object.toString(), 
                                               message.toString());                    
                    } catch (EmailException e) {
                        e.printStackTrace();
                    }
                }else{
                    timerService.createTimer(MAXIMUM_INACTIVE_PERIOD_BEFORE_REMOVAL - inactivityPeriod, userToCheck);
                }
            }
        }
    }
} 

Troubeshooting for JBoss AS 6


When using timer service with JBoss AS 6, you may run into the following exception : 

21:40:29,665 WARN  [com.arjuna.ats.arjuna] ARJUNA-12140 Adding multiple last resources is disallowed.
Current resource is com.arjuna.ats.internal.arjuna.abstractrecords.LastResourceRecord@1bc46c
21:40:29,665 WARN  [org.hibernate.util.JDBCExceptionReporter] SQL Error: 0, SQLState: null
21:40:29,665 ERROR [org.hibernate.util.JDBCExceptionReporter] Could not enlist in transaction
on entering meta-aware object!; - nested throwable:
(javax.transaction.SystemException: java.lang.Throwable: Unabled to enlist resource,
see the previous warnings. 
....

The explanation is that since JBoss AS 6, the server default behavior seems to allow only one datasource at a time. The thing is timer service already use a datasource to persist timer ... To solve this issue, you'll need to edit the %JBOSS_HOME%\server\%YOUR_SERVER_PROFILE%\deploy\transaction-jboss-beans.xml file and add the following property : 

<property name="allowMultipleLastResources">true</property>

in the following bean : 

<bean name="CoreEnvironmentBean" class="com.arjuna.ats.arjuna.common.CoreEnvironmentBean">

15 comments:

  1. Hello, My name is Alan. I am from Lima - Peru.
    A very good blog. Iam interested in learning how to create a web service and then integrate it to intalio to generate dynamic metadata for example. Can you help me?. Thanks.

    ReplyDelete
  2. Hi Alan,

    I would be glad to help you. Do you need help to create your web service or do you already have a web service you need to integrate to Intalio?

    ReplyDelete
  3. Thanks for your answer. I need help to create a web service (maybe you can show me an example), and then how to integrate to intalio. My mail is jacj_cool@hotmail.com. Again, Thanks a lot.

    ReplyDelete
  4. Thanks for your excellent post, your troubleshooting-tip saved my day (after a week that wasn't saved ;)

    ReplyDelete
  5. Hi Michael,

    I'm glad my post could help you. This is what motivates me to spare some of my free time blogging around ;-)

    ReplyDelete
  6. Hi KH Yiu,

    I had problems with ARJUNA-12140, but your tip proved correct. However, when I run my code, the server.log now contains the following message:

    "ARJUNA-12141 Multiple last resources have been added to the current transaction. This is transactionally unsafe and should not be relied upon. Current resource is com.arjuna.ats.internal.arjuna.abstractrecords.LastResourceRecord@1381983"

    Can I disregard this message? What's underneath it? I know the AS needs to persist (internally?) its timers, but why do I need to enable smthg that looks like it shouldn't be done this way? What would be the correct way, then?
    I'm also trying to find out how to use timers in a JBoss cluster, but that is a different topic.

    ReplyDelete
  7. Hi pi,

    To be honest, I've never noticed this message in the server.log file. But I'm pretty sure you can disregard it as it's rather about a warning message. By the way, I'm wondering whether this problem still exist in JBoss AS 7. I'll give it a shot when I get some free time.

    ReplyDelete
    Replies
    1. Hi KH Yiu,

      thx a lot for your tip, it saves a lot of time, but i have the warning msg too. Do you know in the meanwhile anything about the reason producing this warning?

      best regards and thanks again
      Franz

      Delete
    2. Hi Franz,

      I am glad it helped you. Regarding the warning messages, I haven't found yet unfortunately. I'll try to search again but I'm currently busy learning a new (to me) framework.

      Anyway, if you find anything, feel free to leave a comment here, I would really appreciate it and read it with interest.

      Cheers

      Delete
  8. Hi All,

    you can easily disable warnings:

    find this file server/default/deploy/jboss-logging.xml

    and change lines:

    logger category="com.arjuna.ats
    level name="ERROR"

    this is the best solution

    Best Regards

    ReplyDelete
    Replies
    1. Thanks for the hint, I've totally forgotten to check about this (shame on me...)!

      Delete
  9. Any idea about this issue? :
    https://community.jboss.org/message/761257#761257

    ReplyDelete
    Replies
    1. Nope. But I think it would definitely be a good thing to search the JBoss community forum. It is my understanding that similar problems were detected and fixed when using message queues within JBoss AS. Maybe the inner mechanics are similar for both features

      Delete
  10. Hi ,

    Nice post . Easy to follow . I've a situation where i deploy a timer service into server which automatically starts and repeats for the specified time say evry 5 mins . how do i configure it.

    ReplyDelete
  11. Sorry for the belated answer,

    In your situation, you just have to instantiate your timer by invoking either the createIntervalTimer( ), or the createTimer( ) method that has 3 arguments.

    I'd suggest you take a look at the TimerService API: http://docs.oracle.com/javaee/6/api/javax/ejb/TimerService.html

    ReplyDelete