9 Replies Latest reply on Aug 1, 2006 7:25 AM by thehunt

    Clustered HASingleton How-To

    monocongo

      Here is an outline of the steps I took in order to use HASingletons in a clustered JBoss application. I hope it will be helpful to others who try to use HASingletons for the first time. The below is based on the approach explained by Ivelin Ivanov in the ONJava article found here: http://www.onjava.com/pub/a/onjava/2003/08/20/jboss_clustering.html. Additional information is provided in order to explain the steps necessary for accessing the HASingleton's operations from other classes.

      Bear in mind that a JBoss HASingleton is not a true Singleton per se in that you won't get a reference to it and call methods on it like you would with a normal Singleton. Instead it is a JMX service (MBean) with managed operations that you invoke somewhat indirectly. Once I finally understood this the whole approach made a whole lot more sense to me.

      The below is working for me using JBoss 3.2.6.


      First you create the HASingleton MBean and configure it to be controlled by a HASingletonController.


      1. Create an MBean which has lifecycle methods for use by JBoss for initialization and shutdown of the service. Name them whatever you like, I used the names startSingleton() and stopSingleton() as in Ivanov's example. Other methods which return values shoud return Objects instead of primitives, due to the mechanism used for invoking them, as we'll see later. Below is an simple example HASingleton MBean interface and class:

      // -------- ExampleMBean interface --------------------------------------------------
      package com.example.mbean;
      
      import org.jboss.system.ServiceMBean;
      
      public interface ExampleMBean
       extends ServiceMBean
      {
       //===================================================================================
       // Managed MBean operations which control the lifecycle of the HASingleton service
       //===================================================================================
      
       /**
       * Starts the service.
       */
       public void startSingleton ();
      
      
       /**
       * Stops the service.
       */
       public void stopSingleton ();
      
      
       //================================================================================
       // Managed MBean operations
       //================================================================================
      
       /**
       * Determine whether or not a user ID is present in the collection.
       *
       * @param userId the user ID to check
       * @return whether or not the specified user ID is present in the collection
       */
       public Boolean isUserIdPresent (String userId);
      
      
       /**
       * Add a user ID to the collection.
       *
       * @param userId the user ID to add to the collection
       */
       public void addUserId (String userId);
      
      
       /**
       * Remove a user ID from the collection.
       *
       * @param userId the user ID to remove from the collection
       */
       public void removeUserId (String userId);
      }
      
      
      
      // -------- Example MBean class ------------------------------------------------------
      
      package com.example.mbean;
      
      import java.util.Vector;
      import org.jboss.system.ServiceMBeanSupport;
      
      
      public class Example
       extends ServiceMBeanSupport
       implements ExampleMBean
      {
       // collection of user IDs
       private Vector m_userIds;
      
       //==================================================================================
       // Managed MBean operations which control the lifecycle of the HASingleton service
       //==================================================================================
      
       /**
       * Starts the service.
       */
       public void startSingleton ()
       {}
      
       /**
       * Stops the service.
       */
       public void stopSingleton ()
       {}
      
      
       /**
       * Constructor.
       */
       public Example ()
       {
       // create the collection of user IDs
       m_userIds = new Vector();
       }
      
      
       //================================================
       // Managed MBean operations
       //================================================
      
       /**
       * Determine whether or not a user ID is present in the collection.
       *
       * @param userId the user ID to check
       * @return whether or not the specified user ID is present in the collection
       */
       public Boolean isUserIdPresent (String userId)
       {
       return new Boolean(m_userIds.contains(userId));
       }
      
      
       /**
       * Add a user ID to the collection.
       *
       * @param userId the user ID to add to the collection
       */
       public void addUserId (String userId)
       {
       // add the user ID if not already present
       if (! m_userIds.contains(userId))
       {
       m_userIds.add(userId);
       }
       }
      
      
       /**
       * Remove a user ID from the collection.
       *
       * @param userId the user ID to remove from the collection
       */
       public void removeUserId (String userId)
       {
       // remove the user ID if it's present
       if (m_userIds.contains(userId))
       {
       m_userIds.remove(userId);
       }
       }
      }
      



      2. In the SAR which contains the HASingleton MBean declare the MBean and an HASingletonController to control it. This is done in the META-INF/jboss-service.xml file of the SAR. For example the below is a jboss-service.xml for the above HASingleton MBean:

      <?xml version="1.0" encoding="UTF-8"?>
      
      <server>
      
       <!-- MBean to hold all user messages received from the queue -->
       <mbean code="com.example.mbean.Example"
       name="example.mbean:service=Example">
       </mbean>
      
       <!-- HASingletonController to run the above MBean as a HASingleton -->
       <mbean code="org.jboss.ha.singleton.HASingletonController"
       name="jboss.hasingleton:service=ExampleSingletonController">
       <depends>jboss:service=DefaultPartition</depends>
       <depends>example.mbean:service=Example</depends>
       <attribute name="TargetName">example.mbean:service=Example</attribute>
       <attribute name="TargetStartMethod">startSingleton</attribute>
       <attribute name="TargetStopMethod">stopSingleton</attribute>
       </mbean>
      
      </server>
      


      3. Put the SAR with the HASingleton MBean classes into the JBOSS/server/all/deploy-hasingleton directory on every node of your cluster.



      To be able to access the HASingleton you need to use a RMIAdaptor which also runs as a HASingleton. This HASingleton RMIAdaptor will locate your HASingletons and enable you to invoke their operations. Essentially it's running on the same node/JVM by virtue of it being an HASingleton itself, so it can "see" the other HASingletons. This is in contrast to the normal approach of using an MBeanServer, which may or may not be running on the same node/JVM as the HASingletons and hence may not be able to see them since they may not be registered.


      1. Copy the jmx-invoker-adaptor-server.sar directory from JBOSS/server/all/deploy into JBOSS/server/all/deploydeploy-hasingleton.


      2. Modify JBOSS/server/all/deploy-hasingleton/jmx-invoker-adaptor-server.sar/META-INF/jboss-service.xml:

      a) rename the JRMPProxyFactory MBean as SingletonInvoker
      b) change the JndiName for the JRMPProxyFactory MBean to jmx/invoker/SingletonRMIAdaptor
      c) add a HASingletonController for the SingletonInvoker
      d) remove the NamingAlias MBean for /jmx/rmi/RMIAdaptor

      Below is the jboss-service.xml I am using:

      <?xml version="1.0" encoding="UTF-8"?>
      
      <!-- $Id: jboss-service.xml,v 1.1.2.10 2004/09/09 04:19:30 osdchicago Exp $ -->
      <server>
      
       <!-- The JRMP invoker proxy configuration for the InvokerAdaptorService -->
       <mbean code="org.jboss.invocation.jrmp.server.JRMPProxyFactory"
       name="jboss.jmx:type=adaptor,name=SingletonInvoker,protocol=jrmp,service=proxyFactory">
       <!-- Use the standard JRMPInvoker from conf/jboss-service.xxml -->
       <depends optional-attribute-name="InvokerName">jboss:service=invoker,type=jrmp</depends>
       <!-- The target MBean is the InvokerAdaptorService configured below -->
       <depends optional-attribute-name="TargetName">
       jboss.jmx:type=adaptor,name=SingletonInvoker
       </depends>
       <!-- Where to bind the RMIAdaptor proxy -->
       <attribute name="JndiName">jmx/invoker/SingletonRMIAdaptor</attribute>
       <!-- The RMI compabitle MBeanServer interface -->
       <attribute name="ExportedInterfaces">org.jboss.jmx.adaptor.rmi.RMIAdaptor,
       org.jboss.jmx.adaptor.rmi.RMIAdaptorExt
       </attribute>
       <attribute name="ClientInterceptors">
       <interceptors>
       <interceptor>org.jboss.proxy.ClientMethodInterceptor</interceptor>
       <interceptor>org.jboss.proxy.SecurityInterceptor</interceptor>
       <interceptor>
       org.jboss.jmx.connector.invoker.client.InvokerAdaptorClientInterceptor
       </interceptor>
       <interceptor>org.jboss.invocation.InvokerInterceptor</interceptor>
       </interceptors>
       </attribute>
       </mbean>
      
      
       <!-- This is the service that handles the RMIAdaptor invocations by routing
       them to the MBeanServer the service is deployed under. -->
       <mbean code="org.jboss.jmx.connector.invoker.InvokerAdaptorService"
       name="jboss.jmx:type=adaptor,name=SingletonInvoker"
       xmbean-dd="">
       <xmbean>
       <description>The JMX Detached Invoker Service</description>
       <class>org.jboss.jmx.connector.invoker.InvokerAdaptorService</class>
      
       <!-- Attributes -->
       <attribute access="read-only"
       getMethod="getName">
       <description>The class name of the MBean</description>
       <name>Name</name>
       <type>java.lang.String</type>
       </attribute>
       <attribute access="read-only"
       getMethod="getState">
       <description>The status of the MBean</description>
       <name>State</name>
       <type>int</type>
       </attribute>
       <attribute access="read-only"
       getMethod="getStateString">
       <description>The status of the MBean in text form</description>
       <name>StateString</name>
       <type>java.lang.String</type>
       </attribute>
       <attribute access="read-write"
       getMethod="getExportedInterfaces"
       setMethod="setExportedInterfaces">
       <description>The interfaces the invoker proxy supports</description>
       <name>ExportedInterfaces</name>
       <type>[Ljava.lang.Class;</type>
       </attribute>
       <attribute access="read-only"
       getMethod="getMethodMap">
       <description>Map(Long hash, Method) of the proxy interface methods</description>
       <name>MethodMap</name>
       <type>java.util.Map</type>
       </attribute>
       <!-- Operations -->
       <operation>
       <description>The start lifecycle operation</description>
       <name>start</name>
       </operation>
       <operation>
       <description>The stop lifecycle operation</description>
       <name>stop</name>
       </operation>
       <operation>
       <description>The detyped lifecycle operation (for internal use only)</description>
       <name>jbossInternalLifecycle</name>
       <parameter>
       <description>The lifecycle operation</description>
       <name>method</name>
       <type>java.lang.String</type>
       </parameter>
       <return-type>void</return-type>
       </operation>
      
       <operation>
       <description>The detached invoker entry point</description>
       <name>invoke</name>
       <parameter>
       <description>The method invocation context</description>
       <name>invocation</name>
       <type>org.jboss.invocation.Invocation</type>
       </parameter>
       <return-type>java.lang.Object</return-type>
       <!-- Uncomment to require authenticated users . Also an AuthorizationInterceptor
       is provided which whill help in authorizing users to make JMX calls at the
       MBean operations level. You will need to write a class that overrides a method
       with the signature
       "public Boolean authorize( Principal caller, Collection roles,String objectname
      ,String opname)"
       is needed to be defined in the attribute 'authorizingClass'
       <descriptors>
       <interceptors>
       <interceptor code="org.jboss.jmx.connector.invoker.AuthenticationInterceptor"
       securityDomain="java:/jaas/jmx-console"/>
       <interceptor code="org.jboss.jmx.connector.invoker.AuthorizationInterceptor"
       authorizingClass="CustomAuthorizationClass"
       securityDomain="java:/jaas/jmx-console"/>
       </interceptors>
       </descriptors>
       -->
       </operation>
       </xmbean>
       <attribute name="ExportedInterfaces">
       org.jboss.jmx.adaptor.rmi.RMIAdaptor,
       org.jboss.jmx.adaptor.rmi.RMIAdaptorExt
       </attribute>
       </mbean>
      
      
       <!-- HASingletonController to run the InvokerAdaptorService MBean as a HASingleton -->
       <mbean code="org.jboss.ha.singleton.HASingletonController"
       name="jboss.hasingleton:service=RMIAdaptorSingletonController">
       <depends>jboss:service=DefaultPartition</depends>
       <depends>jboss.jmx:type=adaptor,name=SingletonInvoker</depends>
       <attribute name="TargetName">jboss.jmx:type=adaptor,name=SingletonInvoker</attribute>
       <attribute name="TargetStartMethod">start</attribute>
       <attribute name="TargetStopMethod">stop</attribute>
       </mbean>
      
      </server>
      


      Now to access the HASingleton in client code (EJBS, Servlets, other MBeans, etc.) I use the following approach:

      1. Lookup the HASingleton RMIApdaptor
      2. Make sure that the HASingleton I want to use is registered
      3. Invoke the operation and cast the returned object, if any, to the expected class (RMIAdaptor.invoke() returns an object, which is why all of our MBean operations must return Objects and not primitives)

      For example:
      String userId = "elvis";
      try
      {
       // create a HashTable of environment properties for the InitialContext
       //
       // NOTE -- These are hard-coded since the using jndi.properties with a URL containing
       // the HA-JNDI port causes the NamingService to fail on start up; hopefully this
       // issue can be resolved and these properties can again be set in jndi.properties.
       Hashtable jndiProperties = new Hashtable();
       jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY,
       "org.jnp.interfaces.NamingContextFactory");
       // use the port specified in the HANamingService entry (currently set in cluster-service.xml)
       jndiProperties.put(Context.PROVIDER_URL, "localhost:11100");
       jndiProperties.put("java.naming.factory.url.pkgs",
       "org.jboss.naming:org.jnp.interfaces");
      
       // get the InitialContext and lookup the SingletonRMIAdaptor
       Context context = new InitialContext(jndiProperties);
       RMIAdaptor rmiAdaptor = (RMIAdaptor) context.lookup("jmx/invoker/SingletonRMIAdaptor");
      
       // if we have an RMIAdaptor and the Example MBean is registered with it then we can invoke operations
       if ((rmiAdaptor != null) && (rmiAdaptor.isRegistered(new ObjectName("example.mbean:service=Example"))))
       {
       // add the message to the list for this user ID
       Boolean isUserIdPresent = rmiAdaptor.invoke(new ObjectName("example.mbean:service=Example"),
       "isUserIdPresent",
       new Object[]{userId},
       new String[]{"java.lang.String"});
      
       // show our result
       if (isUserIdPresent.booleanValue())
       {
       System.out.println("User ID " + userId + " is in the collection");
       }
       else
       {
       System.out.println("User ID " + userId + " is not in the collection");
       }
       }
       else
       {
       // log the error
       m_logger.error("Unable to find an RMIAdaptor with a registered Example HASingleton MBean");
      
       // throw an exception
       throw new Exception("Unable to find an RMIAdaptor with a registered Example HASingleton MBean");
       }
      }
      catch (MBeanException e)
      {
       // log the error
       m_logger.error("Unable to invoke the Example.isUserIdPresent() method", e);
      
       // throw an exception
       throw new Exception("Unable to invoke the Example.isUserIdPresent() method", e);
      }
      catch (ReflectionException e)
      {
       // log the error
       m_logger.error("Unable to invoke the Example.isUserIdPresent() method", e);
      
       // throw an exception
       throw new Exception("Unable to invoke the Example.isUserIdPresent() method", e);
      }
      catch (InstanceNotFoundException e)
      {
       // log the error
       m_logger.error("Unable to invoke the Example.isUserIdPresent() method", e);
      
       // throw an exception
       throw new Exception("Unable to invoke the Example.isUserIdPresent() method", e);
      }
      catch (MalformedObjectNameException e)
      {
       // log the error
       m_logger.error("Unable to invoke the Example.isUserIdPresent() method", e);
      
       // throw an exception
       throw new Exception("Unable to invoke the Example.isUserIdPresent() method", e);
      }
      catch (NamingException e)
      {
       // log the error
       m_logger.error("Unable to invoke the Example.isUserIdPresent() method", e);
      
       // throw an exception
       throw new Exception("Unable to invoke the Example.isUserIdPresent() method", e);
      }
      catch (RemoteException e)
      {
       // log the error
       m_logger.error("Unable to invoke the Example.isUserIdPresent() method", e);
      
       // throw an exception
       throw new Exception("Unable to invoke the Example.isUserIdPresent() method", e);
      }
      



      The above approach works well for me except in the case when the master node of the cluster goes down and the HASingletons are restarted on the new master node of the cluster. The state of the HASingletons is lost since they are started fresh. For example in the above example if the master node running the HASingleton goes down then the collection of user IDs will be lost. For this reason I would not recommend using HASingletons if you need to maintain state, I have learned this lesson the hard way. My understanding at this point is that a stateful Singleton is not a good idea and instead an Entity Bean should be used. If anyone can suggest a solution to this problem, other than rewriting everything using Entity Beans (which, I fear, might in fact be the only solution) then I'd certainly appreciate it. You can post suggestions in the following thread:http://www.jboss.org/index.html?module=bb&op=viewtopic&t=55787

      I hope the above is helpful.


      --James