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">

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.