Introduction
The aim of this wiki is to explain how to deploy EJB3 timer beans in a cluster. Before explaining the required steps to achieve this, let's explain a bit of background information that will help understand better the deployment strategy.
Background Information
The most obvious way to deploy a timer in a cluster, whether it's an EJB or not, is to deploy it as a cluster wide singleton so that at one point, only one node is calling the scheduled method in the target service or EJB. As explained in HASingletonDeployer wiki, there're two ways to deploy a cluster wide singleton, or HA singleton, in a cluster: putting the deployment archive in deploy-hasingleton/ folder, or making your service/bean depend on the HA singleton deployer barrier. Let's look at these two HA singleton deployment strategies and their suitability for deploying EJB2 and EJB3 beans as HA singletons.
Deploying an EJB2 or EJB3 bean by putting the deployment archive in deploy-hasingleton/ folder is not recommended, specially if the deployments are scoped, see JBAS-5284. Secondly, from a deployment ordering perspective, an EJB jar archive deployed in deploy-hasingleton/ would be deployed much earlier than if the EJB jar was deployed in deploy/ directory, so you might encounter some dependency issues because some services needed by the EJB jar might still not be ready.
In theory, it's possible to make your EJBs dependant on the HA singleton deployer barrier however, as explained in HASingletonDeployer, this might not work correctly with EJB containers that do quite a bit of work in their create/destroy lifecycle phases. This was actually the case of EJB2 containers, where most of the work (such as creating interceptors, instance cache and pools,...etc) was done in the create phase, so trying to deploy EJB2 beans as HA singletons via the barrier method would not work correctly. With EJB3 containers however, this is different. Most the work is done is done in the start/stop lifecycle phases, so we can indeed deploy EJB3 beans this way.
So, to cut a long story short, we can deploy EJB3 beans (but not EJB2 beans), including EJB3 timer beans, as HA singletons via the barrier dependency method.
Initial Setup
Step 1. Grab a copy of JBoss AS 4.2.x+ or JBoss EAP 4.2.x/4.3.x+ and create two new server configurations (based on /all).
Server Side Setup
Step 2. For this example to work, both server configurations need to share a database so that when one node goes down, the other node can continue calling the bean's @Timeout method. There's no need to change the DefaultDS for this example. Instead, it's recommended that once the shared database is created, a datasource pointing to this database it's created with JNDI name SharedDS. Example oracle-ds.xml used to create this example:
<?xml version="1.0" encoding="UTF-8"?> <datasources> <local-tx-datasource> <jndi-name>SharedDS</jndi-name> <connection-url>jdbc:oracle:thin:@localhost:1521:XE</connection-url> <driver-class>oracle.jdbc.driver.OracleDriver</driver-class> <user-name>galder</user-name> <password>pass</password> <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter</exception-sorter-class-name> <metadata> <type-mapping>Oracle9i</type-mapping> </metadata> </local-tx-datasource> </datasources>
Step 3. Open deploy/ejb-deployer.xml file in both server configurations and change the jboss.ejb:service=EJBTimerService,persistencePolicy=database service to point to the shared database, and if using Oracle database use the appropriate database persistence plugin:
<mbean code="org.jboss.ejb.txtimer.DatabasePersistencePolicy" name="jboss.ejb:service=EJBTimerService,persistencePolicy=database"> <!-- DataSourceBinding ObjectName --> <depends optional-attribute-name="DataSource">jboss.jca:service=DataSourceBinding,name=SharedDS</depends> <!-- The plugin that handles database persistence --> <attribute name="DatabasePersistencePlugin">org.jboss.ejb.txtimer.OracleDatabasePersistencePlugin</attribute> <!-- The timers table name --> <attribute name="TimersTable">TIMERS</attribute> </mbean>
Step 4. Start both server configurations and verify that they form a cluster.
Deployment Setup
Step 5. Create an EJB3 SLSB containing methods to schedule and cancel a timer in the Timer service, and a method annotated with @Timeout. First of all, here's the interface of the bean:
package com.acme.ejb3.timer; import java.io.Serializable; import java.util.Date; import javax.ejb.Timer; /** * TimerManagerProxy. * * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a> */ public interface TimerManagerProxy { public void scheduleTimer(Date expiration, Serializable info); public void scheduleTimer(long initialDuration, long intervalDuration, Serializable info); public void cancelTimer(Serializable info); public void timeoutHandler(Timer timer) throws Exception; }
And this is the bean implementation:
package com.acme.ejb3.timer; import java.io.Serializable; import java.util.Collection; import java.util.Date; import javax.annotation.Resource; import javax.ejb.EJB; import javax.ejb.Remote; import javax.ejb.SessionContext; import javax.ejb.Stateless; import javax.ejb.Timeout; import javax.ejb.Timer; import org.jboss.logging.Logger; /** * TimerManagerProxyBean. * * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a> */ @Stateless @Remote(TimerManagerProxy.class) public class TimerManagerProxyBean implements TimerManagerProxy { private static final Logger log = Logger.getLogger(TimerManagerProxyBean.class); private @Resource SessionContext ctx; public void scheduleTimer(Date expiration, Serializable info) { log.info("Create single action timer [info=" + info + "]"); ctx.getTimerService().createTimer(expiration, info); } public void scheduleTimer(long initialDuration, long intervalDuration, Serializable info) { log.info("Create initial+interval timer [info=" + info + "]"); ctx.getTimerService().createTimer(initialDuration, intervalDuration, info); } public void cancelTimer(Serializable info) { log.info("Cancel timer [info=" + info + "]"); Collection<Timer> timers = ctx.getTimerService().getTimers(); for (Timer timer : timers) { if (timer.getInfo().equals(info)) { log.info("Timer[info=" + info + "] found, cancelling..."); timer.cancel(); log.info("Timer[info=" + info + "] cancelled"); } } } @Timeout public void timeoutHandler(Timer timer) throws Exception { log.debug("Received timer event: " + timer.getInfo()); Date date = new Date(System.currentTimeMillis()); log.info("Current time is: " + date + ", origin: timeoutHandler"); } }
Step 6. Now that the bean is ready, we need to make the bean dependant on the HA singleton barrier to get the bean to only deploy in 1 node in the cluster. To do that, create a META-INF/jboss.xml file within the EJB jar and put the following:
<?xml version="1.0" encoding="UTF-8"?> <jboss xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss_5_0.xsd" version="3.0"> <enterprise-beans> <session> <ejb-name>TimerManagerProxyBean</ejb-name> <depends>jboss.ha:service=HASingletonDeployer,type=Barrier</depends> </session> </enterprise-beans> </jboss>
Deployment
Step 7. Deploy the EJB jar in both server configurations and check that the timer bean has only been started in the first node started, who is the coordinator of the cluster. You should see a message like this in the coordinator of the cluster only:
13:10:12,608 INFO [EJBContainer] STARTED EJB: com.acme.ejb3.timer.TimerManagerProxyBean ejbName: TimerManagerProxyBean
Client Code
Step 8. It's time to write the client code that should contain two methods: first, a method to schedule the timer to be executed periodically, and secondly, a method to cancel the timer:
package com.acme.ejb3.timer; import java.util.Properties; import javax.naming.Context; import javax.naming.InitialContext; import junit.framework.TestCase; /** * ScheduleClusterTimerTest. * * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a> */ public class ScheduleClusterTimerTest extends TestCase { public void testClusterScheduleIntervalTimer() throws Exception { InitialContext ctx = createClusterLoginInitialContext(); TimerManagerProxy timer = (TimerManagerProxy) ctx.lookup("TimerManagerProxyBean/remote"); timer.scheduleTimer(10000, 5000, "ClusterTimer"); } public void testClusterCancelTimer() throws Exception { InitialContext ctx = createClusterLoginInitialContext(); TimerManagerProxy timer = (TimerManagerProxy) ctx.lookup("TimerManagerProxyBean/remote"); timer.cancelTimer("ClusterTimer"); } private InitialContext createClusterLoginInitialContext() throws Exception { Properties env = new Properties(); env.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory"); env.setProperty(Context.URL_PKG_PREFIXES, "jboss.naming:org.jnp.interfaces"); env.setProperty("jnp.partitionName", "DefaultPartition"); return new InitialContext(env); } }
Execution
Step 9. Execute testClusterScheduleIntervalTimer() method on the client code and you should see the timer being scheduled in the coordinator of the cluster:
13:23:22,320 INFO [TimerManagerProxyBean] Create initial+interval timer [info=ClusterTimer] 13:23:32,422 INFO [TimerManagerProxyBean] Current time is: Wed Jul 23 13:23:32 CEST 2008, origin: timeoutHandler 13:23:37,327 INFO [TimerManagerProxyBean] Current time is: Wed Jul 23 13:23:37 CEST 2008, origin: timeoutHandler 13:23:42,327 INFO [TimerManagerProxyBean] Current time is: Wed Jul 23 13:23:42 CEST 2008, origin: timeoutHandler 13:23:47,327 INFO [TimerManagerProxyBean] Current time is: Wed Jul 23 13:23:47 CEST 2008, origin: timeoutHandler ...
Step 10. If you now stop the coordinator of the cluster, you should see TimerManagerProxyBean being started in the new coordinator of the cluster, which by the default is the server started after the first one. You should also see the @Timeout method continue to be executed periodically:
13:24:20,331 INFO [DefaultPartition] New cluster view for partition DefaultPartition (id: 2, delta: -1) : [127.0.0.3:1099] 13:24:20,334 INFO [DefaultPartition] I am (127.0.0.3:1099) received membershipChanged event: 13:24:20,334 INFO [DefaultPartition] Dead members: 1 ([127.0.0.2:1099]) 13:24:20,334 INFO [DefaultPartition] New Members : 0 ([]) 13:24:20,334 INFO [DefaultPartition] All Members : 1 ([127.0.0.3:1099]) 13:24:20,475 INFO [EJBContainer] STARTED EJB: com.acme.ejb3.timer.TimerManagerProxyBean ejbName: TimerManagerProxyBean ... 13:24:20,886 INFO [TimerManagerProxyBean] Current time is: Wed Jul 23 13:24:20 CEST 2008, origin: timeoutHandler 13:24:25,439 INFO [TimerManagerProxyBean] Current time is: Wed Jul 23 13:24:25 CEST 2008, origin: timeoutHandler 13:24:30,439 INFO [TimerManagerProxyBean] Current time is: Wed Jul 23 13:24:30 CEST 2008, origin: timeoutHandler ...
Step 11. Finally, execute testClusterCancelTimer() and you should see the timer being stopped in the remaining node in the cluster:
13:24:42,738 INFO [TimerManagerProxyBean] Cancel timer [info=ClusterTimer] 13:24:42,738 INFO [TimerManagerProxyBean] Timer[info=ClusterTimer] found, cancelling... 13:24:42,740 INFO [TimerManagerProxyBean] Timer[info=ClusterTimer] cancelled
Comments