How to configure an MDB singleton in a cluster

In this tutorial we will provide details how to configure MDB deployments as Clustered Singleton MDBs on WildFly 10 or later.

First of all, what is a “Clustered Singleton MDBs” ? they are a special kind of MDB which is deployed in a cluster; however only one node is active to consume messages serially. When the node fails, another node is elected as the active node’s “Clustered Singleton MDBs” and starts consuming the messages.

The simplest way, though not the only one, to configure an MDB as Clustered Singleton is via the special element which can be added in the jboss-ejb3.xml file as follows:

<?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:c="urn:clustering:1.1"
               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>
        <c:clustering>
            <ejb-name>HelloWorldQueueMDB</ejb-name>
            <c:clustered-singleton>true</c:clustered-singleton>
        </c:clustering>
    </assembly-descriptor>
</jboss:ejb-jar>

Here is a sample MDB from the quickstart which can be configured to be clustered as Singleton:

package org.jboss.as.quickstarts.mdb;

import java.util.logging.Logger;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

 
@MessageDriven(name = "HelloWorldQueueMDB", activationConfig = {
        @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "queue/HELLOWORLDMDBQueue"),
        @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
        @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge")})
public class HelloWorldQueueMDB implements MessageListener {

    private static final Logger LOGGER = Logger.getLogger(HelloWorldQueueMDB.class.toString());

    /**
     * @see MessageListener#onMessage(Message)
     */
    public void onMessage(Message rcvMessage) {
        TextMessage msg = null;
        try {
            if (rcvMessage instanceof TextMessage) {
                msg = (TextMessage) rcvMessage;
                LOGGER.info("Received Message from queue: " + msg.getText());
            } else {
                LOGGER.warning("Message of wrong type: " + rcvMessage.getClass().getName());
            }
        } catch (JMSException e) {
            throw new RuntimeException(e);
        }
    }
}

In order to let it work, you need to create the JMS destination and expose its entries:

jms-queue add --queue-address=HELLOWORLDMDBQueue --entries=queue/HELLOWORLDMDBQueue,java:jboss/exported/jms/queue/HELLOWORLDMDBQueue

Now as you deploy the MDB singleton, you will notice from the Server’s log that it has been distributed to the cluster and one server was elected as singleton provider:

2017-10-19 15:28:21,325 INFO  [org.wildfly.clustering.server] (DistributedSingletonService - 1) WFLYCLSV0003: master:server-one elected as the singleton provider of the org.wildfly.ejb3.clustered.singleton service

If you check the JMS resources for this node, you will see that the default number of consumers have been created for this server:

[domain@localhost:9990 /] /host=master/server=server-one/subsystem=messaging-activemq/server=default/jms-queue=HELLOWORLDMDBQueue:read-resource(include-runtime=true)
{
    "outcome" => "success",
    "result" => {
        "consumer-count" => 15,
        "dead-letter-address" => "jms.queue.DLQ",
        "delivering-count" => 0,
        "durable" => true,
        "entries" => [
            "queue/HELLOWORLDMDBQueue",
            "java:jboss/exported/jms/queue/HELLOWORLDMDBQueue"
        ],
        "expiry-address" => "jms.queue.ExpiryQueue",
        "legacy-entries" => undefined,
        "message-count" => 0L,
        "messages-added" => 0L,
        "paused" => false,
        "queue-address" => "jms.queue.HELLOWORLDMDBQueue",
        "scheduled-count" => 0L,
        "selector" => undefined,
        "temporary" => false
    }
}

On the other hand, if you try to check the JMS resources on another server in the cluster, you will see there are no available consumer for this resource:

[domain@localhost:9990 /] /host=master/server=server-two/subsystem=messaging-activemq/server=default/jms-queue=HELLOWORLDMDBQueue:read-resource(include-runtime=true)
{
    "outcome" => "success",
    "result" => {
        "consumer-count" => 0,
        "dead-letter-address" => "jms.queue.DLQ",
        "delivering-count" => 0,
        "durable" => true,
        "entries" => [
            "queue/HELLOWORLDMDBQueue",
            "java:jboss/exported/jms/queue/HELLOWORLDMDBQueue"
        ],
        "expiry-address" => "jms.queue.ExpiryQueue",
        "legacy-entries" => undefined,
        "message-count" => 0L,
        "messages-added" => 0L,
        "paused" => false,
        "queue-address" => "jms.queue.HELLOWORLDMDBQueue",
        "scheduled-count" => 0L,
        "selector" => undefined,
        "temporary" => false
    }
}

Now, as proof of concept, let’s stop the node who is managing the Singleton:

[domain@localhost:9990 /] /host=master/server-config=server-one:stop
{
    "outcome" => "success",
    "result" => "STOPPING"
}

As you can see from the logs of the server-two, the ownership of the Singleton has been moved:

2017-10-19 15:30:05,910 INFO  [org.wildfly.clustering.server] (DistributedSingletonService - 1) WFLYCLSV0003: master:server-two elected as the singleton provider of the org.wildfly.ejb3.clustered.singleton service

Now the server-two got transferred the consumer which can handle message consuming:

[domain@localhost:9990 /] /host=master/server=server-two/subsystem=messaging-activemq/server=default/jms-queue=HELLOWORLDMDBQueue:read-resource(include-runtime=true)
{
    "outcome" => "success",
    "result" => {
        "consumer-count" => 15,
        "dead-letter-address" => "jms.queue.DLQ",
        "delivering-count" => 0,
        "durable" => true,
        "entries" => [
            "queue/HELLOWORLDMDBQueue",
            "java:jboss/exported/jms/queue/HELLOWORLDMDBQueue"
        ],
        "expiry-address" => "jms.queue.ExpiryQueue",
        "legacy-entries" => undefined,
        "message-count" => 0L,
        "messages-added" => 0L,
        "paused" => false,
        "queue-address" => "jms.queue.HELLOWORLDMDBQueue",
        "scheduled-count" => 0L,
        "selector" => undefined,
        "temporary" => false
    }
}

Using the @ClusteredSingleton annotation

As final note, it is worth mentioning that you can use the @org.jboss.ejb3.annotation.ClusteredSingleton in your MDB class whichrequires no extra configuration at the server, apart from running the service in a cluster:

@ClusteredSingleton
  
@MessageDriven(name = "HelloWorldQueueMDB", activationConfig = {
        @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "queue/HELLOWORLDMDBQueue"),
        @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
        @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge")})
public class HelloWorldQueueMDB implements MessageListener {
 
    private static final Logger LOGGER = Logger.getLogger(HelloWorldQueueMDB.class.toString());
 
    /**
     * @see MessageListener#onMessage(Message)
     */
    public void onMessage(Message rcvMessage) {
        TextMessage msg = null;
        try {
            if (rcvMessage instanceof TextMessage) {
                msg = (TextMessage) rcvMessage;
                LOGGER.info("Received Message from queue: " + msg.getText());
            } else {
                LOGGER.warning("Message of wrong type: " + rcvMessage.getClass().getName());
            }
        } catch (JMSException e) {
            throw new RuntimeException(e);
        }
    }
}