2.6 Quick Tour of JMX
The easiest way to understand JMX is with a simple but thorough example. This example uses Tivoli's TMX4J JMX implementation, but the code should be identical regardless of who the JMX vendor is. The remainder of this chapter is dedicated to demonstrating the major features of JMXMBeans, the MBeanServer, monitors, and notificationsvia the design and implementation of a simple manageable server application.50
2.6.1 todd, the Time of Day Daemon
todd is a simple daemon that does a simple job. It accepts client connections and sends them the current date and time whenever they ask for it. Figure 2.5 shows todd's static structure.
Figure 2.5 Class Structure of the Unmanaged Time of Day Daemon
Three classes form todd's core: Server, Listener, and Session. The Server class is responsible for starting a listener and assigning sessions to incoming connections. The Listener class accepts incoming connections and queues them for the server. The Session class is responsible for responding to client messages received via a connection. Figure 2.6 illustrates the interaction among todd's classes.
2.6.2 todd Management
todd satisfies its basic functional requirements by accepting client connections and supplying the current date and time on demand, but it's not manageable. We have no way of knowing the total number of sessions that todd has handled, how many sessions are active, how many connections are queued, how long each session has been running, and so on. We can't stop and start the listener without killing todd, and any active sessions along with it. We can't change the size of the connection queue or session pool without recompiling the code. If todd were to become a popular service, our administrator colleagues would not be happy with us.
Figure 2.6 todd Component Interactions During a Client Session
JMX lets us build manageability into our Java applications. MBeans capture the management interface of the resources that management systems need to monitor and control. The MBeanServer provides a common registry and naming model for all of an application's MBeans. MBean services exist to autonomously monitor MBean attribute values and fire notifications when constraints are violated. Notifications provide both a means to alert an administrator of a problem, and an implicit invocation mechanism that developers can use to make an application self-managing.
In the next few sections we will apply JMX to todd and transform it into a manageable application that will delight the system administration staff, or at least keep them from paging us in the middle of the night. Because the goal of this tour is to demonstrate the major features of JMX in an application, we will introduce them without a lot of explanation. Subsequent chapters will provide details of all the features we use in our example.
Before we start, it would be good to have an idea of the sort of manageability requirements the new version of todd has to satisfy. Table 2.1 provides a minimal list. Modifying todd to satisfy these requirements will involve the following activities: designing and implementing the necessary MBeans, incorporating an MBeanServer into todd, and wiring up the monitors and NotificationListener instances that are needed to complete the implementation.
Table 2.1 Basic todd Management Requirements
Aspect |
Requirement |
Description |
Server control |
Stop/start |
Stop and start the server without killing the todd process or any active sessions. |
|
Shutdown |
Shut down the server, killing the todd process and any active sessions. |
Server data |
Total connections |
Show how many connections the server has handled so far. |
|
Uptime |
Show how long the server has been running. |
|
Active sessions |
Show how many sessions are currently active. |
Session pool management |
Grow pool |
Increase the size of the session pool by a specified amount. |
|
Empty pool management |
Stop the server when the pool becomes empty; restart the server when the pool contains at least two sessions. |
2.6.3 todd's MBeans
From the requirements in Table 2.1, it's pretty clear that we're concerned with only a couple of todd's resources: the server itself, and the pool of sessions that todd uses to service connections. Because JMX uses MBeans to represent managed resources, it follows that we need to make the Server and SessionPool classes MBeans. There are several ways to accomplish that goal; the simplest is to make them standard MBeans. In general a standard MBean for a given resource is defined by a Java interface named MyResourceMBean and a Java class, MyResource, which implements the MyResourceMBean interface. MyResourceMBean defines the MBean's management interfacethat is, the attributes and methods that JMX makes available to management applications.
It's clear from the requirements what the server's management interface should look like:
public interface ServerMBean { void shutdown(); void start(); void stop(); Integer getConnections(); Integer getSessions(); Long getUptime(); }
A JMX management interface contains attributes and operations. In a standard MBean those attributes and operations are expressed as methods in a Java interface. Methods that have the following form:
AttributeType getAttributeName(); void setAttributeName();
define an attribute named AttributeName that takes values of type AttributeType. If the setAttributeName() method is missing, the attribute is read-only; if the getAttributeName() method is missing, the attribute is write-only. Any method in the MBean interface that doesn't define an attribute defines an operation for the management interface.
The first three methods in the ServerMBean interface define operations that a management application can invoke on ServerMBean instances. The remaining methods define attributes; specifically they define three read-only attributes: Connections, Sessions, and Uptime.
The implementation of the ServerMBean interface in the Server class is straightforward:
public class Server implements ServerMBean, NotificationListener { private SessionPool sessions; private SortedSet connectionQueue; private Listener listener; private int connections; // incremented for each new connection private int tzero; // System.currentTimeMillis at Server start ... // Other Server methods that aren't part of the MBean interface /** * Shut down the server, killing the process and any active sessions */ public void shutdown() { System.exit(0); } /** * Start a listener thread that will queue incoming connections */ public void start() { listener = new Listener(connectionQueue); listener.start(); } /** * Stop the server's listener thread; active sessions continue to * handle requests */ public void stop() { listener.stopListening(); } /** * Connections attribute getter * @returns total number of connections handled */ public Integer getConnections() { return new Integer(connections); } /** * Sessions attribute getter * @returns number of active sessions */ public Integer getSessions() { int as = sessions.getAvailableSessions().intValue(); int sz = sessions.getSize().intValue(); return new Integer(sz as); } /** * Uptime attribute getter * @returns number of milliseconds since the server was started */ public Long getUptime() { return new Long(System.currentTimeMillis() tzero); } }
The shape of the SessionPool management interface requires a little more thought. Clearly it needs a grow operation to satisfy the first session pool management requirement. What about the empty pool management requirement? We could specify a monitorPoolSpace operation that would provide the necessary behavior, but as we'll see in a moment, that would be reinventing a perfectly good JMX wheel. Instead, let's just satisfy the underlying data requirement by providing access to the number of sessions left in the pool. A glance back at the ServerMBean implementation will reveal that we've already assumed that this information, along with the session pool size, is available, so we have this:
public interface SessionPoolMBean { void grow(int increment); Integer getAvailableSessions(); Integer getSize(); }
todd uses java.util.Set as the underlying data structure for the SessionPool implementation:
public class SessionPool implements SessionPoolMBean { private static final int DEFAULT_POOLSIZE = 8; private Set sessions; private int size; /** * Default constructor creates a SessionPool instance of size * DEFAULT_POOLSIZE */ public SessionPool() { this(DEFAULT_POOLSIZE); } /** * Creates a SessionPool instance of the specified size and * fills it with Session instances */ public SessionPool(int size) { this.size = size; sessions = new HashSet(size); fill(); } /** * Increase the number of Session instances in SessionPool by * increment * @param increment the number of Session instances to add to * the pool */ public synchronized void grow(int increment) { for (int i = 0; i < increment; i++) { Session s = new Session(this); sessions.add(s); } size = size + increment; } /** * AvailableSessions attribute getter * @returns number of sessions remaining in the pool */ public Integer getAvailableSessions() { return new Integer(sessions.size()); } /** * Size attribute getter * @returns size of the session pool */ public Integer getSize() { return new Integer(size); } ... // Other SessionPool methods that are not part of the MBean // interface }
You've probably noticed that all of our attribute getters return Java numeric wrapper typesInteger, Long, and so on. We do that so that we can use JMX monitors such as GaugeMonitor, which we'll use in the implementation of the empty pool management requirement, to observe the values taken by those attributes. Note also that we have kept the operations in the ServerMBean and SessionPoolMBean interfaces very simple.
2.6.4 Incorporating an MBeanServer
Now that we've got some MBeans, what do we do with them? Because a JMX-based management application can access MBeans only if they are registered with an MBeanServer, we should register them. Unfortunately, we don't have an MBeanServer in todd to register any MBeans with at the moment. That problem can be solved with a single line of code in todd's main() method:
MBeanServer mbs = MBeanServerFactory.createMBeanServer();
In the interest of minimizing the impact of incorporating the MBeanServer, we will instantiate both the Server and the SessionPool MBeans and then register them, rather than creating and automatically registering them via the MBeanServer. Server is instantiated in main(), and SessionPool is created as part of the Server instantiation:
public static void main(String[] args) throws Exception { MBeanServer mbs = MBeanServerFactory.createMBeanServer(); Server server = new Server(mbs); ObjectName son = new ObjectName("todd:id=Server"); mbs.registerMBean(server, son); ... while (server.isActive()) { Connection k = server.waitForConnection() server.activateSession(k); } } public Server(MBeanServer mbs) throws Exception { this.mbs = mbs; connectionQueue = new TreeSet(); connections = 0; sessions = new SessionPool(); ObjectName spon = new ObjectName("todd:id=SessionPool"); mbs.registerMBean(sessions, spon); active = true; tzero = System.currentTimeMillis(); }
The MBeanServer associates an ObjectName instance with each MBean. We've registered Server and SessionPool under the names todd:id=Server and todd:id=SessionPool. The portion of the name to the left of the colon is the domain, which is an arbitrary string that is opaque to the MBeanServer but may have meaning to one or more management applications. On the right are the key properties, a set of name/value pairs that help distinguish one MBean from another. Together they must form a unique name, within a given MBeanServer, for the associated MBean.
2.6.5 Monitoring todd
We still haven't satisfied the SessionPool empty pool management requirement. The SessionPool MBean tells us how many sessions are left in the pool. What we need is a way to react to these two events: (1) AvailableSessions has become zero, and (2) AvailableSessions has increased from zero to one or more.
In JMX, events are called notifications. Every JMX notification has a class and a type; its class is either javax.management.Notification or one of its subclasses; its type is String expressed in dot notationfor example, jmx.mbean.registered. Notifications are handled by calls to one of the MBeanServer's addNotificationListener() methods:
public void addNotificationListener(ObjectName objname, NotificationListener listener, NotificationFilter filter, Object handback); public void addNotificationListener(ObjectName objname, ObjectName listener, NotificationFilter filter, Object handback);
The only difference between the two method calls is the second parameter. In the first version the second parameter is a reference to a NotificationListener instancethat is, an instance of a class that implements the NotificationListener interface. In the second version the second parameter is the ObjectName instance of an MBean that implements NotificationListener.
Careful readers will have noticed that the Server class implements ServerMBean and NotificationListener. Satisfying the empty pool management requirement involves stopping and starting the server that makes the Server class. These actions provide the stop() and start() methods with a natural place to handle the notifications that trigger those actions. The NotificationListener interface declares a single method, handleNotification(). Here is the Server implementation of the method:
public void handleNotification(Notification n, Object hb) { String type = n.getType(); if (type.compareTo (MonitorNotification.THRESHOLD_LOW_VALUE_EXCEEDED) == 0) { stop(); } else if (type.compareTo (MonitorNotification.THRESHOLD_HIGH_VALUE_EXCEEDED) == 0) { if (isActive() == false) start(); } }
Now all that's required is a mechanism for generating notifications at the appropriate times. We need something that will monitor SessionPool's AvailableSessions attribute, send a notification when it becomes zero, and then send another notification later when AvailableSessions increases to one or more.
The JMX GaugeMonitor class provides just such a mechanism. A GaugeMonitor instance is configured to monitor a specific attribute of an MBean registered with the MBeanServer. The GaugeMonitor class has two thresholds: high and low. A MonitorNotification instance with type jmx.monitor.threshold.high is sent when the attribute value increases to or past the high threshold value. Similarly, when the attribute value decreases to or below the low threshold value, a MonitorNotification instance with type jmx.monitor.threshold.low is sent.
The Server.configureMonitor() method sets up a GaugeMonitor instance that completes the implementation of the empty pool management requirement:
public static void configureMonitor(MBeanServer mbs) throws Exception { ObjectName spmon = new ObjectName("todd:id=SessionPoolMonitor"); mbs.createMBean("javax.management.monitor.GaugeMonitor", spmon); AttributeList spmal = new AttributeList(); spmal.add(new Attribute("ObservedObject", new ObjectName("todd:id=SessionPool"))); spmal.add(new Attribute("ObservedAttribute", "AvailableSessions")); spmal.add(new Attribute("GranularityPeriod", new Long(10000))); spmal.add(new Attribute("NotifyHigh", new Boolean(true))); spmal.add(new Attribute("NotifyLow", new Boolean(true))); mbs.setAttributes(spmon, spmal); mbs.invoke( spmon, "setThresholds", new Object[] { new Integer(1), new Integer(0)}, new String[] { "java.lang.Number", "java.lang.Number" }); mbs.addNotificationListener( spmon, new ObjectName("todd:id=Server"), null, new Object()); mbs.invoke(spmon, "start", new Object[] {}, new String[] {}); }
The first two lines here create and register a GaugeMonitor MBean named todd:id=SessionPoolMonitor. The next seven lines set attributes that tell GaugeMonitor which attribute of which MBean should be monitored (ObservedAttribute or ObservedObject), how often (GranularityPeriod, in milliseconds), and whether or not to send a notification on high-threshold and low-threshold violations. Then we invoke the setThresholds() method, via the MBeanServer, to set the actual high and low threshold values. Finally, we make the server listen for session pool monitor notifications and start the gauge monitor.
2.6.6 Browser Control
The new version of todd satisfies all of the management requirements specified in Table 2.1. There is, however, one more issue to address: We would like to be able to control todd interactively. For example, an administrator should be able to connect to todd from home to check the values of key MBean attributes, stop the server, increase the size of the session pool, and restart the server if necessary.
Although there are as many ways to address this issue as there are communication protocols, most JMX implementations provide an HTTP adapter that allows a user to "surf" an MBeanServerinspecting MBean attributes, invoking operations, and so onusing a standard Web browser. Here's the code necessary to start the TMX4J HTTP adapter:
ObjectName httpon = new ObjectName("adapters:id=Http"); mbs.createMBean("com.tivoli.jmx.http_pa.Listener", httpon); mbs.invoke(httpon, "startListener", new Object[] {}, new String[] {});
Once the adapter has been started, you can connect to the MBeanServer via a Web browser. In the case of the TMX4J HTTP adapter, you would direct your browser to http://<your-server>:6969. You can change the port the adapter listens on in the jmx.properties file that the TMX4J implementation reads on startup. Figure 2.7 shows the TMX4J view of the todd MBeans.
Figure 2.7 The TMX4J HTTP Adapter's MBean View