Friday, October 2, 2009

Exposing Spring's DefaultMessageListenerContainer via JMX in JBoss

Recently I had received the request to allow the admins to change some of the properties on our queue listeners on the fly in our JBoss environment. The intent here being that they wanted to be able to change the number of active consumers on a particular server instance without having to cycle the instance.

Well... a little while and some keystrokes later we had something like this :




<!-- Listener for requests -->
    <bean id="requestListener" class="com.test.jms.MessageListener"/>
    <bean id="requestListenerContainer"
          class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="qcf"/>
        <property name="destinationResolver" ref="destinationResolver"/>
        <property name="destinationName" value="queue/REQUEST_QUEUE"/>
        <property name="messageListener" ref="attributeRequestListener"/>
        <property name="concurrentConsumers" value="5"/>
        <property name="maxConcurrentConsumers" value="20"/>
...
    </bean>

    <bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
        <property name="locateExistingServerIfPossible" value="true"/>
    </bean>

    <!-- Expose through JMX -->
    <bean id="jmxExporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="server" ref="mbeanServer"/>
        <property name="beans">
            <map>
                <entry key="cma:name=requestListenerContainer" value-ref="requestListenerContainer"/>
            </map>
        </property>

   </bean>


After restarting and looking at the jmx-console, my values were all present and so I, in a moment of sloppiness, decided the job was complete and off I went onto my next task. A couple of days later I heard that this functionality was not working and was in fact throwing the following exception when any updates were attempted.


10:12:21,787 ERROR [ContainerBase] Servlet.service() for servlet HtmlAdaptor threw exception
java.lang.ClassNotFoundException: org.springframework.jms.support.destination.DestinationResolver
 at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1358)
 at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1204)
 at org.jboss.util.propertyeditor.PropertyEditors.convertValue(PropertyEditors.java:250)
 at org.jboss.jmx.adaptor.control.Server.setAttributes(Server.java:190)
 at org.jboss.jmx.adaptor.html.HtmlAdaptorServlet.updateAttributes(HtmlAdaptorServlet.java:236)
 at org.jboss.jmx.adaptor.html.HtmlAdaptorServlet.processRequest(HtmlAdaptorServlet.java:98)
 at org.jboss.jmx.adaptor.html.HtmlAdaptorServlet.doPost(HtmlAdaptorServlet.java:82)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:710)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
 at org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
 at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
 at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
 at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:179)
 at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:525)
 at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:84)
 at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
 at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:104)
 at org.jboss.web.tomcat.service.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:157)
 at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
 at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:241)
 at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:844)
 at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:580)
 at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
 at java.lang.Thread.run(Thread.java:595)


A quick look and we can see that JBoss isn't able to locate the Spring's DestinationResolver. A gander at the Spring/JMX documentation (1) showed a couple of options were available. The MBeanExporter(2) exposes the assembler through which you can configure the properties and operations to be exposed.

The first one I tried as a PC was to directly name the properties that I wanted to expose so as a quick test I added the following :


                                                                                    
   <bean id="jmxExporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="server" ref="mbeanServer"/>
        <property name="beans">
            <map>
                <entry key="cma:name=requestListenerContainer" value-ref="requestListenerContainer"/>
            </map>
        </property>
        <property name="assembler">
            <bean class="org.springframework.jmx.export.assembler.MethodNameBasedMBeanInfoAssembler">
                <property name="managedMethods">
                    <value>setMaxConcurrentConsumers,getMaxConcurrentConsumers </value>
                </property>
            </bean>
        </property> 
  </bean>
                                           

Restarted and viola, when I go view the JMX info for this bean the only items available to me were maxConcurrentConsumers (Read/Write) and destinationResolver (Read only) and the operations start and stop. I ran some manual tests against the system to ensure that I was able to update the maxConcurrentConsumers without error and that the changes stuck.

That was all fine and well, but I'm not sure that I want that info stored in XML in the context file, so rather than stopping here I created a template interface that stores the items that I want exposed

package com.test.jmx;

/**
 * Provides a template to expose Springs DefaultMessageListenerContainer
 * The following items are not supported via this interface and should be
 * enabled in the inheriting interfaces to keep the dependancies of this the
 * common package to a miniumum :
 *
 * 
 */

public interface IJmxExposedDefaultMessageListenerContainer {

    //
    // managed properties
    //


    int getMaxConcurrentConsumers();
    void setMaxConcurrentConsumers(int maxConcurrentConsumers);
   
    //
    // Managed Operations
    //

    void start();
    void stop();
}

and then wired this up to the assembler thusly :

          <property name="assembler">
            <bean class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler">
                <property name="managedInterfaces">
                    <value>com.ideas.cma.common.jmx.IJmxExposedDefaultMessageListenerContainer</value>
                </property>
            </bean>
        </property>  

And off we went...

Resources :
(3) http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/jms/listener/DefaultMessageListenerContainer.html

2 comments:

Cyrille Le Clerc said...

If you are interested, we have packaged a JMX enabled DefaultMessageListener with a monitoring JSP and an Hyperic plugin at ManagedSpringJmsDefaultMessageListenerContainer .

We also packaged JMX enabled components like Commons DBCP DataSource, util.concurrent ExecutorService / ThreadPoolExecutor, JMS ConnectionFactory, Spring CachingConnectionFactory and Spring DefaultMessageListenerContainer, etc
All the components are configurable with Spring XML namespace configuration and have JSP monitoring pages as well as Hyperic HQ plugins.

All the details at Google Code - XebiaManagementExtras.

Hope this helps,

Cyrille (Xebia)

trisbez said...

Sorry for the late reply. Very cool! Thanks