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.

Let’s start from a simple EJB 3 Timer which is scheduled to be executed 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 is triggered once on every node of the cluster:

ejb 3 timer cluster

Clustered EJB 3 Timer Service

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>

Once done with the Datasource we will configure the EJB 3 timer service to use a datasource clustered store, related to our PostgreSQL Datasource. 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>

Please note the partition property which allows breaking a large cluster up into several smaller clusters, in order to improve the performance.

Fine. Now start up the application server and deploy the EJB 3 Timer to a cluster. You will see that now the timer is being triggered in 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 just need to call TimerService.getAllTimers() in the context of the bean.  WildFly will then check that the wildfly.ejb.timer.refresh.enabled property is set to true. If so, timers will be refreshed. One way to set programmatically this property is via EJB Interceptors:

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