Configuring a Remote HornetQ JMS Server as a Resource Adapter

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

External

Internal

Overview

This article describes the configuration required to make a cluster of two HornetQ-based JMS nodes, running as messaging subsystems within EAP 6.4 instances, accessible to a third JBoss EAP 6.4. The HornetQ cluster will be accessible to the third JBoss instance via a resource adapter, and thus available to the MDBs deployed on that JBoss instance.

The procedure consists in declaring netty connections from the resource adapter node to the HornetQ nodes and using those connections from a pooled connection which exposes the remote cluster as a resource adapter.

Procedure

Declare the Outbound Socket Bindings

On the client node (the node the resource adapter gets deployed on) declare the outbound socket bindings corresponding to the HornetQ nodes. Note that the configuration shown below implies that the HornetQ node 1 runs with a port offset of 100, and HornetQ node 2 runs with a port offset of 200.

<outbound-socket-binding name="remote-hornetq-node-1-socket-binding">
    <remote-destination host="${remote.hornetq.node.one.address:127.0.0.1}" 
                        port="${remote.hornetq.node.one.hornetq.port:5545}"/>
</outbound-socket-binding>
<outbound-socket-binding name="remote-hornetq-node-2-socket-binding">
    <remote-destination host="${remote.hornetq.node.two.address:127.0.0.1}"
                        port="${remote.hornetq.node.two.hornetq.port:5645}"/>
</outbound-socket-binding>

Declare the Netty Connectors

Declare the netty connectors corresponding to the HornetQ nodes in the messaging subsystem configuration.

<subsystem xmlns="urn:jboss:domain:messaging:...">
    <hornetq-server>
        ...
        <connectors>
            ...
            <netty-connector name="remote-hornetq-node-1-connection" 
                             socket-binding="remote-hornetq-node-1-socket-binding"/>
            <netty-connector name="remote-hornetq-node-2-connection" 
                             socket-binding="remote-hornetq-node-2-socket-binding"/>
        </connectors>

       ...
</subsystem>     

Declare the Resource Adapter

Declare the resource adapter that will balance among the HornetQ nodes. It is declared as a pooled connection factory. The semantics of the system properties used in declaration will be explained in the System Properties section.

<subsystem xmlns="urn:jboss:domain:messaging:...">
    <hornetq-server>
        ...
        <jms-connection-factories>
            ...
            <pooled-connection-factory name="hornetq-remote-ra">
                <inbound-config>
                    <use-jndi>true</use-jndi>
                    <jndi-params>
java.naming.factory.initial=org.jboss.naming.remote.client.InitialContextFactory;
java.naming.provider.url=
remote://${remote.hornetq.node.one.address}:${remote.hornetq.node.one.remoting.port},
remote://${remote.hornetq.node.two.address}:${remote.hornetq.node.two.remoting.port};
java.naming.security.principal=${remoting.user.name};
java.naming.security.credentials=${remoting.password}
                    </jndi-params>
                </inbound-config>  
                <transaction mode="xa"/>  
                <user>${jms.user.name}</user>  
                <password>${jms.password}</password>  
                <connectors>  
                    <connector-ref connector-name="remote-hornetq-node-1-connection"/>  
                    <connector-ref connector-name="remote-hornetq-node-2-connection"/>  
                </connectors>  
                <entries>  
                    <entry name="java:/RemoteJmsXA"/>  
                </entries>  
            </pooled-connection-factory>
       </jms-connection-factories>
       ...
</subsystem>     

The above declaration refers to a series of system properties, which must be set appropriately and are explained in the next section.

Note that "java.naming.security.principal" and "java.naming.security.credentials" used in <jndi-params> are not necessary if remoting security is disabled. This is how remoting security can be disabled:

Disabling Remoting Authentication

Likewise, <user>${jms.user.name}</user> and <password>${jms.password}</password> are not used if HornetQ security is disabled on the remote hosts. This is how HornetQ security can be disabled:

Disabling HornetQ Security

TODO. Properly explain in the proper place what <use-jndi>true</use-jndi> is. Use the HornetQ documentation. <use-jndi>true</use-jndi> establishes that JNDI should be used to locate the destination for incoming connections. Link back to MDB#useJNDI

TODO. Properly explain in the proper place what <ha>true</ha> is. Use the HornetQ documentation. Link back to MDB#hA

System Properties

<system-properties>
    <property name="remote.hornetq.node.one.address" value="127.0.0.1"/>
    <property name="remote.hornetq.node.one.hornetq.port" value="5545"/>
    <property name="remote.hornetq.node.one.remoting.port" value="4547"/>
    <property name="remote.hornetq.node.two.address" value="127.0.0.1"/>
    <property name="remote.hornetq.node.two.hornetq.port" value="5645"/>
    <property name="remote.hornetq.node.two.remoting.port" value="4647"/>
    <property name="remoting.user.name" value="some-remoting-user"/>
    <property name="remoting.password" value="some-remoting-password"/>
    <property name="jms.user.name" value="some-jms-user"/>
    <property name="jms.password" value="some-jms-password"/>
</system-properties>
  • "remote.hornetq.node.one.address" the address of the first HornetQ node.
  • "remote.hornetq.node.one.hornetq.port" the netty connector port of the first HornetQ node.
  • "remote.hornetq.node.one.remoting.port" the remoting port of the first HornetQ node. The default value is 4447 and if the node runs with an offset of 100, the value should be 4547.
  • "remote.hornetq.node.two.address" the address of the second HornetQ node.
  • "remote.hornetq.node.two.hornetq.port" the netty connector port of the second HornetQ node.
  • "remote.hornetq.node.two.remoting.port" the remoting port of the second HornetQ node. The default value is 4447 and if the node runs with an offset of 200, the value should be 4647.
  • "remoting.user.name"/"remoting.password" the remoting credentials. Remoting can be configured to allow unauthenticated access as described here: "Disabling Remoting Authentication".
  • "jms.user.name"/"jms.password" credentials required to create JMS connections. HornetQ can be configured to allow anonymous connections, as described here: "HornetQ Configuration - Disabling Security"

Resource Adapter Deployment Smoke Test

22:44:44,499 INFO  [org.hornetq.ra] (MSC service thread 1-7) HornetQ resource adaptor started
22:44:44,499 INFO  [org.jboss.as.connector.services.resourceadapters.ResourceAdapterActivatorService$ResourceAdapterActivator] (MSC service thread 1-7) IJ020002: Deployed: file://RaActivatorhornetq-remote-ra
22:44:44,500 INFO  [org.jboss.as.connector.deployment] (MSC service thread 1-1) JBAS010401: Bound JCA ConnectionFactory [java:/RemoteJmsXA]

Configure an MDB to Use the Remote JMS Server Resource Adapter

This section explains how to configure an MDB to receive messages from a destination deployed on the remote HornetQ JMS server. The MDB container must be configured to use the resource adapter that exposes the remote HornetQ JMS provider. A fully working example is available here:

https://github.com/NovaOrdis/playground/tree/master/jee/ejb/mdb-with-remote-resource-adapter

TODO. Only messages sent to one node end being processed by the MDB. Figure this out. It is probably something related to load balancing. Or, do I need to cluster the remote HQ nodes?

@ResourceAdapter

The key configuration element is @ResourceAdapter:

import org.jboss.ejb3.annotation.ResourceAdapter;
...
@MessageDriven(...)
@ResourceAdapter("hornetq-remote-ra")

For more details about the @ResourceAdapter annotation see:

MDB - @ResourceAdapter

Activation Configuration

@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),  
    @ActivationConfigProperty(propertyName = "destination", propertyValue = "/jms/queue/remote-inbound"),  
    @ActivationConfigProperty(propertyName = "useJNDI",propertyValue = "true")  
})

"/jms/queue/remote-inbound" is a queue deployed on the remote HornetQ server, and it is accessible to the MDB under the original name because the MDB container infrastructure performs a remote lookup in the external JNDI context. TODO more on "useJNDI" and the relationship with the pooled connection factory configuration.

MDB Deployment Smoke Test

23:10:00,636 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-6) JBAS015876: Starting deployment of "mdb-remote-ra.jar" (runtime-name: "mdb-remote-ra.jar")
23:10:00,816 INFO  [org.jboss.as.ejb3] (MSC service thread 1-6) JBAS014142: Started message driven bean 'MDB' with 'hornetq-remote-ra' resource adapter

High Availability

TODO

Sending Messages to Remote Destinations

This section describes the configuration required if local components need to send messages to remote destinations, via the connection factory exposed by the resource adapter deployed as described above. The components that want to send messages will must use "java:/RemoteJmsXA" connection factory, deployed as described in the "Declare the Resource Adapter" section. However, the destinations are deployed on the remote HornetQ nodes, and without additional configuration, the local components do not have access to them. In order to enable access, we need to declare an external JNDI context, as described below:

Declare the External JNDI Context

Declare locally the external JNDI Context that contains the remote destinations on the remote HornetQ nodes. Details on how to declare (import) an external JNDI context are available here:

Importing an External JNDI Context

Declare org.hornetq Dependency

A dependency on "org.hornetq" is required to correctly resolve the HornetQ destination implementation classes, after they are looked up in JNDI. The dependency can be declared in the component's jboss-deployment-structure.xml deployment descriptor:

<jboss-deployment-structure>
    <deployment>
        <dependencies>
            <module name="org.hornetq" slot="main"/>
        </dependencies>
    </deployment>
</jboss-deployment-structure>

or globally, among the global modules:

<subsystem xmlns="urn:jboss:domain:ee:...">
    ...
    <global-modules>
        <module name="org.hornetq" slot="main"/>
    </global-modules>
</subsystem>

For more details on global modules, see Global Modules.

Sending/Receiving Messages from an Arbitrary Component

The JNDI API usage to access objects bound in the external JNDI context (in this particular case, destinations) is described here:

Accessing the External JNDI Context

An example that uses InitialContext to look up the destinations in JNDI is available here:

https://github.com/NovaOrdis/playground/tree/master/jboss/messaging/sending-and-receiving-from-remote-destinations

Another equivalent example that uses the @Resource annotations to get the remote destination injected by the container is available here

https://github.com/NovaOrdis/playground/tree/master/jboss/messaging/sending-and-receiving-from-injected-remote-destinations

Sending Messages from an MBD

The following example contains an MDB that receives messages from a remote "inbound" queue, processes them, and sends response messages on a remote "outbound" queue. Both "inbound" and "outbound" queues are deployed on the remote HornetQ server. A working example is available here:

https://github.com/NovaOrdis/playground/tree/master/jee/ejb/mdb-with-remote-resource-adapter-that-replies-to-response-queue

Relevant sections:

1. The MDB must be configured to receive inbound messages from the remote inbound queue over the remote HornetQ server resource adapter as described here: Configure an MDB to Use the Remote JMS Server Resource Adapter.

2. Inject the ConnectionFactory to use to send response messages with:

@Resource(name = "java:/RemoteJmsXA")
private ConnectionFactory cf;

3. Inject the external JNDI context to use to lookup the remote destinations with:

@Resource(lookup = "java:global/remote-hornetq")
private InitialContext externalContext;

where the "lookup" value is the same value used to configure the external-context name (see <external-context> name).

4. The code that sends the response message is similar to (connection closing, error handling, etc. are rudimentary and it should not use as such in production code):

@Override
public void onMessage(Message message) {

    ...

    // process the message

    // compute the response 

   Queue responseQueue = (Queue)externalContext.lookup("/jms/queue/remote-outbound");

   c = cf.createConnection();

   Session s = c.createSession(false, Session.AUTO_ACKNOWLEDGE);

   MessageProducer p = s.createProducer(responseQueue);

   Message m = s.createTextMessage("...");

   p.send(m);

   c.close();

  ...

}

5. Declare the dependency of the deployment artifact (or a global dependency) on org.hornetq as described here "Declare org.hornetq Dependency".

Variations on How JNDI Names are Handled

In some situations is important that the code of the component does not change between deployments in collocated and remote topologies, so specific methods have to be employed to make sure that the JNDI names stay the same between deployments. Two methods are presented below:

Using System Properties that are Resolved at Runtime

This example shows code that can be deployed in collocated and remote JMS environments, by using JNDI names defined as system properties. This behavior is possible because JBoss can be configured to resolve system properties declared in annotations (see "annotation-property-replacement configuration option of the "ee" subystem"), so the deployment can be configured with different destination and connection factory JNDI names by declaring different system property values. Note that annotation-property-replacement must be enable for the example to work, and it is disabled by default.

https://github.com/NovaOrdis/playground/tree/master/jee/ejb/mdb-that-replies-to-response-queue-collocated-and-remote

Also, note that "org.hornetq" dependency must be added as a global module for this to work, otherwise the JNDI lookup of the remote destination will fail:

<subsystem xmlns="urn:jboss:domain:ee:1.2">
    <global-modules>
        <module name="org.hornetq" slot="main"/>
    </global-modules>
    <annotation-property-replacement>true</annotation-property-replacement>
    ...
</subsystem>

Using "naming" System Lookups

A variation of the above example uses "common" hardcoded JNDI names, and we use mapping at "naming" subsystem level to bind the remote destinations under different names in the local JNDI space.

https://github.com/NovaOrdis/playground/tree/master/jee/ejb/mdb-that-replies-to-response-queue-collocated-and-remote-jndi-mapping

Also, note that "org.hornetq" dependency must be added as a global module for this to work, otherwise the JNDI lookup of the remote destination will fail:

<subsystem xmlns="urn:jboss:domain:ee:1.2">
    <global-modules>
        <module name="org.hornetq" slot="main"/>
    </global-modules>
    <annotation-property-replacement>true</annotation-property-replacement>
    ...
</subsystem>