Creating persistent clustered EJB 3 Timers

In this tutorial we will learn how to configure a Timer EJB 3 Service on a cluster of servers. You will need WildFly application server and a Database supporting READ_COMMITTED or SERIALIZABLE isolation level.

A simple Timer

Let’s start from a simple EJB 3 Timer which will fire every 10 seconds:

@Singleton
@Startup
public class UserRegistry {
 
    @Schedule(hour = "*", minute = "*", second = "*/10", info ="Every minute timer",persistent=true)
    public void printDate() {	 
		System.out.println(" It is " + new java.util.Date().toString());
    }
 
}

Plain and easy, however the problem arises when you try to deploy the EJB 3 timer on a cluster. if you try to do that, you will see that the timer fires once on every node of the cluster:

ejb 3 timer cluster

To avoid that, we need to change a bit the configuration of the ejb3 subsystem. Let’s see how to do that.

Moving to Clustered Timers

You can persist the state of EJB Timers in WildFly by binding the EJB 3 Timer service to a Database storage, using an isolation mode like READ_COMMITTED or SERIALIZABLE which prevents concurrent change by multiple nodes of the cluster. Let’s start at first to define a Datasource which is fit for our purpose:

<datasources>
    <datasource jndi-name="java:/PostGreDS" pool-name="PostgrePool">
        <connection-url>jdbc:postgresql://localhost:5432/postgres</connection-url>
        <driver>postgres</driver>
        <security>
            <user-name>postgres</user-name>
            <password>postgres</password>
        </security>
    </datasource>
    <drivers>
        <driver name="postgres" module="org.postgres">
            <driver-class>org.postgresql.Driver</driver-class>
        </driver>
    </drivers>
</datasources>

Next, we will configure the EJB 3 timer service to use a PostgreSQL datasource as storage for Timers. Execute the following commands from WildFly CLI:

 /subsystem=ejb3/service=timer-service/database-data-store=clustered-store:add(datasource-jndi-name=java:/PostGreDS, database=postgresql, partition=timer)
 /subsystem=ejb3/service=timer-service:write-attribute(name=default-data-store,value=clustered-store)

As a result, the configuration will now include the following timer-service section:

  <timer-service thread-pool-name="default" default-data-store="clustered-store">
        <data-stores>
            <file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
            <database-data-store name="clustered-store" datasource-jndi-name="java:/PostGreDS" database="postgresql" partition="timer"/>
        </data-stores>
    </timer-service>

Two things to notice:

  • The partition: A node will only capture timers from other nodes that have the same partition name. This allows to improve the performance by breaking a large cluster into several smaller clusters with different partition names.
  • The default-data-store property in this example set the clustered-store as default store for your timers.

If you prefer, you can choose the data-store to use at application level in the jboss-ejb3.xml file. Example:

<?xml version="1.1" encoding="UTF-8"?>
<jboss:ejb-jar xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
               xmlns="http://java.sun.com/xml/ns/javaee"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns:timer="urn:timer-service:1.0"
               xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-2_0.xsd
                     http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd"
               version="3.1"
               impl-version="2.0">
    <assembly-descriptor>
            <timer:timer>
                <ejb-name>*</ejb-name>
                <timer:persistence-store-name>clustered-store</timer:persistence-store-name>
            </timer:timer>
        </assembly-descriptor>
</jboss:ejb-jar>

Clustered Timer in action

Next, we will test our timers by deploying the EJB 3 Timer to a cluster. You will see that now the timer fires just on ONE node of the cluster, hence guaranteeing a single execution of the Timer:

ejb 3 timer cluster

We can check on our Database console that the table “jboss_ejb_timer” has been created:

postgres-# \d jboss_ejb_timer
                                Table "public.jboss_ejb_timer"
             Column             |            Type             | Collation | Nullable | Default 
 id                             | character varying           |           | not null | 
 timed_object_id                | character varying           |           | not null | 
 initial_date                   | timestamp without time zone |           |          | 
 repeat_interval                | bigint                      |           |          | 
 next_date                      | timestamp without time zone |           |          | 
 previous_run                   | timestamp without time zone |           |          | 
 primary_key                    | character varying           |           |          | 
 info                           | text                        |           |          | 
 timer_state                    | character varying           |           |          | 
 schedule_expr_second           | character varying           |           |          | 
 schedule_expr_minute           | character varying           |           |          | 
 schedule_expr_hour             | character varying           |           |          | 
 schedule_expr_day_of_week      | character varying           |           |          | 
 schedule_expr_day_of_month     | character varying           |           |          | 
 schedule_expr_month            | character varying           |           |          | 
 schedule_expr_year             | character varying           |           |          | 
 schedule_expr_start_date       | character varying           |           |          | 
 schedule_expr_end_date         | character varying           |           |          | 
 schedule_expr_timezone         | character varying           |           |          | 
 auto_timer                     | boolean                     |           |          | 
 timeout_method_declaring_class | character varying           |           |          | 
 timeout_method_name            | character varying           |           |          | 
 timeout_method_descriptor      | character varying           |           |          | 
 calendar_timer                 | boolean                     |           |          | 
 partition_name                 | character varying           |           | not null | 
 node_name                      | character varying           |           |          | 

Also, the Timer entry has been persisted in a record:

select id, timed_object_id,next_date,previous_run, schedule_expr_minute,timeout_method_declaring_class from jboss_ejb_timer;
                  id                  |                   timed_object_id                    |      next_date      |    previous_run     | schedule_expr_minute |  timeout_method_declaring_class 
d035cfa1-8f2b-4b90-a601-41798d2302be | ee-ejb-server-timer.ee-ejb-server-timer.UserRegistry | 2021-03-19 14:14:00 | 2021-03-19 14:13:00 | *                    | com.itbuzzpress.ejb.UserRegi
stry
(1 row)

Refreshing EJB Timers

To programmatically refresh EJB timers, the application need to call TimerService.getAllTimers() in the context of the bean.  WildFly will then check that the wildfly.ejb.timer.refresh.enabled property evaluates to true. If so, timers will be refreshed. One way to set programmatically this property is via EJB Interceptors. Example:

import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;

/**
 * An interceptor to enable programmatic timer refresh across multiple nodes.
 */
@Interceptor
public class RefreshInterceptor {
    @AroundInvoke
    public Object intercept(InvocationContext context) throws Exception {
        context.getContextData().put("wildfly.ejb.timer.refresh.enabled", Boolean.TRUE);
        return context.proceed();
    }
}

Source Code

You can find an example EJB Timer here: https://github.com/fmarchioni/mastertheboss/tree/master/ejb/ejb-timer