The Source for Java Technology Collaboration
User: Password:



Eamonn McManus's Blog

Community: JDK Archives


A query language for the JMX API

Posted by emcmanus on April 25, 2008 at 08:21 AM | Permalink | Comments (0)

The JMX API is being updated by JSR 255. That JSR is currently planned to be part of Java SE 7, and some of the API changes it defines have started to appear in JDK 7. So far, the main one is a Query Language. Here's what that is and what it's for.

The JMX API has always included the idea of queries. The idea is that you can tell the queryNames method to filter the set of objects that it returns, using an object that implements the QueryExp interface. For example, you can pick out only those MBeans that have an attribute called Enabled with the value true and an attribute called Owner with the value "Duke".

The recommended way to obtain QueryExp objects is using the static methods of the Query class. That way, your QueryExp consists only of standard classes that must be present on all JMX implementations and there are no worries about classes that are present on the client but not the server.

Up until now, the way to code the query I described above was this:

 QueryExp query =
     Query.and(Query.eq(Query.attr("Enabled"), Query.value(true)),
               Query.eq(Query.attr("Owner"), Query.value("Duke")));
    

While it's possible to decipher that and determine that it does indeed mean what I described, it isn't very easy. The idea of the query language is that you can get the same QueryExp object like this:

 QueryExp query = Query.fromString("Enabled = true and Owner = 'Duke'");
    

Much easier to understand! (Let me stress that this is just an alternative way of writing existing queries. It doesn't introduce any new types of query.)

The query language is closely based on the WHERE clause of SQL SELECT queries. Here are the other examples from the specification:

Message = 'OK'
Selects MBeans that have a Message attribute whose value is the string OK.
FreeSpacePercent < 10
Selects MBeans that have a FreeSpacePercent attribute whose value is a number less than 10.
FreeSpacePercent < 10 and WarningSent = false
Selects the same MBeans as the previous example, but they must also have a boolean attribute WarningSent whose value is false.
SpaceUsed > TotalSpace * (2.0 / 3.0)
Selects MBeans that have SpaceUsed and TotalSpace attributes where the first is more than two-thirds the second.
not (FreeSpacePercent between 10 and 90)
Selects MBeans that have a FreeSpacePercent attribute whose value is not between 10 and 90, inclusive.
FreeSpacePercent not between 10 and 90
Another way of writing the previous query.
Status in ('STOPPED', 'STARTING', 'STARTED')
Selects MBeans that have a Status attribute whose value is one of those three strings.
Message like 'OK: %'
Selects MBeans that have a Message attribute whose value is a string beginning with "OK: ". Notice that the wildcard characters are SQL's ones. In the query language, % means "any sequence of characters" and _ means "any single character". In the rest of the JMX API, these correspond to * and % respectively.
instanceof 'javax.management.NotificationBroadcaster'
Selects MBeans that are instances of javax.management.NotificationBroadcaster, as reported by MBeanServer.isInstanceOf.
like 'mydomain:*'
Selects MBeans whose ObjectNames have the domain mydomain.

If you're familiar with SQL, all of these should be familiar, except the last two, which have no SQL equivalent.

The full specification also includes a formal grammar, which I won't reproduce here. I'll just say that I got to liberate my repressed inner compiler geek when writing the parser.

Other uses for the Query Language

Apart from making it easier to write code that does queries, a standard query language is very practical for tools like JConsole or VisualVM that might want to allow the user to select a subset of MBeans using a query. A simple text field can now be used to do this.

The query language also provides one solution to a problem with the existing Query API. The methods of the Query class allow you to construct objects that implement QueryExp. But if you have such an object, there is no easy way to look inside to see what kind of query it is. As well as Query.fromString, the new API includes Query.toString to do the reverse transformation. With these two methods you can for example save a query in a text file, or send it over a text-based protocol like SOAP, and reconstruct it later.

(You might be wondering why we bothered defining Query.toString. Couldn't we just have said that the standard toString() method would do the right thing? The main reason it can't is that ObjectName is a QueryExp, but the syntax to include an ObjectName in a query is for example "like '*:type=Foo,*'", while ObjectName.toString() will be just "*:type=Foo,*".)

Custom queries

Nothing prevents you from writing your own class that implements QueryExp and giving it to queryNames. Although custom queries are powerful, they're also discouraged. Most queries that you want can be composed out of the standard set. The problem with custom implementations is that, if you want to do your query remotely, you need to arrange for the implementation class to be present on both client and server.

This is why Query.toString and Query.fromString don't specify any handling for non-standard queries. Although various possibilities could be imagined for what that might look like, people could be seduced into using custom queries without realizing the deployment headaches that that can lead to down the road.

Why SQL?

You might be wondering why the query language is based on SQL, which is a database query language. Is that really appropriate to query management objects?

Although it's far from obvious, the original JMX query API was closely based on SQL too. In fact, the places where the query language described here differs from SQL are essentially the places where the JMX query API has changed since its original version. One strong historical hint here is that the Reference Implementation has always used SQL syntax in the toString() methods of the various QueryExp classes, even going as far as to replace * and ? with their SQL equivalents % and _.

Quite apart from this, SQL is familiar to very many programmers, and is also the inspiration for the query languages used by the Java Persistence API (JPA) and the Java Message Service (JMS).

Evolution of the standard queries

The set of available standard queries has expanded slightly over time. The original set was defined in version 1.0 of the JMX API, way back in 2000.

In version 1.2 of the API, we made ObjectName implement QueryExp, which gave users a way to match ObjectName patterns themselves, and also meant that you could use queryNames to find MBeans that match a pattern AND another pattern, and various other Boolean combinations. This is the last available standalone version, and the version that was included with Java SE 5.0.

In Java SE 6 we added Query.isInstanceOf.

In Java SE 7 (assuming plausibly that that includes JSR 255, the new JMX API), in addition to the query language, we're including the ability to use dotted attribute expression like A.b.c, with the same meaning as for monitors. So for example you could find out which memory pools still have init the same as committed in their MemoryUsage using code like this:

MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName poolPattern = new ObjectName("java.lang:type=MemoryPool,*");
QueryExp q = Query.fromString("Usage.init = Usage.committed");
// or: Query.eq(Query.attr("Usage.init"), Query.attr("Usage.committed"));
Set<ObjectName> names = mbs.queryNames(poolPattern, q);

If you're sending either isInstanceOf or dotted attribute expressions over a network connection, you need to have some way of knowing that the other end supports those. Otherwise, you have to avoid using either of these.

(We could add a method, say Query.isSupported(QueryExp, MBeanServerConnection) that tells you whether the given MBean Server supports the given query, by examining its specification version. But most of the time, either you wouldn't know what to do if it returned false; or you would know what to do, and you could just do that always.)

A pattern matching problem

One area where I'm not sure the query language does the right thing is with patterns. In the existing API, if you want to query for all MBeans that have a State attribute that is a string in parentheses, you would use
Query.match(Query.attr("State"), Query.value("(*)")).
In the Query Language, you can instead write
Query.fromString("State like '(%)'").
As before, that's much easier to read, but what's the story with * versus %?

Query.match uses the well-known "shell-style" wildcards, where ? matches any single character and * matches zero or more characters. On the other hand, the LIKE operator in the SQL standard uses the characters _ and % for the same thing. Both the Java Persistence Query Language from Java EE and JMS Message Selectors have a LIKE operator that uses the SQL characters. So people familiar with these will expect the LIKE operator in the JMX query API to work the same way. On the other hand, people who are familiar with Query.match or shell wildcards or ObjectName wildcards will expect the other convention. It's particularly messy when you compare a query that matches ObjectNames, corresponding to ObjectName.apply, with one that matches strings:

QueryExp objectNameQuery = new ObjectName("mydomain:*");
System.out.println(Query.toString(objectNameQuery));
// prints: LIKE 'mydomain:*'
// this is not a standard SQL query so we don't have to respect precedent

QueryExp objectNameStringQuery =
    Query.match(Query.attr("Name.canonicalName"), Query.value("mydomain:*"));
System.out.println(Query.toString(objectNameStringQuery));
// prints: Name.canonicalName like 'mydomain:%'

Never mind the inconsistent case of LIKE (which I just noticed). Can we really live with * in some places and % in others?

This is just the beginning

You can expect other new features from 2.0 to show up in the JDK 7 snapshots in the coming months. Namespaces, Event Service, localization support, you name it!

[Tags: jdk.]



Public Review of Web Services Connector for JMX Agents

Posted by emcmanus on February 18, 2008 at 08:52 AM | Permalink | Comments (2)

The Public Review of JSR 262, "Web Services Connector for JMX Agents", is underway, and there's a new snapshot of the Reference Implementation that corresponds to the Public Review specification.

Jean-François's blog has the full details.

[Tags: .]

JMX API 2.0 Early Draft Review

Posted by emcmanus on December 28, 2007 at 02:14 AM | Permalink | Comments (5)

The first draft of JSR 255 is out! This defines version 2.0 of the JMX API. We're planning to integrate it into the Java SE 7 platform, subject to the approval of the Expert Group for that platform.

Here's a summary of the important changes. If you're interested, I'd encourage you to download the draft and look at the summary in the Overview Description, which has links into the relevant parts of the API.

This draft contains all the major features that we are planning to add in this version of the API. If there's anything you'd like to see changed, this would be a very good time to let us know!

Namespaces and Cascading

The concept of namespaces is new. All MBeans whose domain begins with foo//, for example foo//com.example:type=CacheController, belong to the namespace foo//. Previously, MBeanServer.queryNames(null, null)) returned a list of all MBeans. Now, MBeans within namespaces do not appear in that list. Additionally, namespaces can contain Virtual MBeans which do not have to exist as Java objects when they are not being accessed. Namespaces allow much improved scalability, especially when accessing certain MBeans is expensive.

Cascading or Federation means that it is straightforward to import MBeans from a remote MBean Server as if they were local. This blog entry gives an overview of what Cascading is about.

Event Service and Notifications

The Event Service provides greater control over notification handling than the default technique using MBeanServer.addNotificationListener. The Event Service separates the logic for notification handling from the particular connector that you are using. You can transport notifications using that connector, or using a completely different transport. See the description of the javax.management.event package for more information.

A new class QueryNotificationFilter allows notifications to be filtered using the existing query subsystem.

Resource injection provides an alternative to implementing the NotificationBroadcaster interface or extending the NotificationBroadcasterSupport class. See the next section.

Annotations and Resource Injection

MBeans can now be defined using annotations. Also, the @Resource annotation allows an MBean to get a reference to its MBeanServer and ObjectName references, as an alternative to implementing MBeanRegistration.

This blog entry provides details and rationale.

Client Contexts and Localization

MBeans now have access to a context that can contain information such as locale or transaction ids. (Note though that there is no explicit support for transactions in the API.)

The descriptions in an MBeanInfo can be localized using a new method MBeanInfo.localizeDescriptions. A way will be provided to cause this method to be called for all MBeans, using a locale communicated by a remote client. The details are still being sorted out so this area of the API will change.

Queries

A new Query Language provides an alternative way to specify queries that is often simpler than constructing QueryExp objects using the static methods of the Query class. (The idea of an SQL-like query language for JMX queries was first proposed by Norbert Lataille and Marc Fleury in 2000, although the language in this draft is not derived from that proposal.)

Attributes appearing in a query can now use a dot (.) to specify a value contained in an attribute of complex type, similar to the existing support in the javax.management.monitor package.

MXBeans

The type mappings can now be customized for any MXBean using annotations or options. Previously the mapping rules were fixed.

Options for StandardMBean and Proxies

A new MBeanOptions class provides control over details of StandardMBean and MXBean proxy operation.

DynamicWrapperMBean

A new interface DynamicWrapperMBean gives better behaviour for MBeans that wrap other objects, in particular as regards class loading.

NotificationManager interface

Three methods of the MBeanServerConnection interface are promoted to a new parent interface NotificationManager, which can be implemented by classes such as EventClient that provide addNotificationListener/removeNotificationListener functionality.

Model MBeans

It is no longer required that the Descriptor in, for example, ModelMBeanAttributeInfo contain redundant descriptorType and name fields. The previously-required values are supplied by default if omitted. (Thanks to Lars Westergren for this contribution.)

Send feedback!

Feel free to comment here, or send mail to jmx-spec-comments@sun.com.

[Tags: ]



Defining MBeans with annotations

Posted by emcmanus on August 31, 2007 at 08:11 AM | Permalink | Comments (9)

The number one question I get about the JMX API at conferences and other public events is whether there will be support for defining MBeans using annotations. People see that they can make EJBs or Web Services just by adding annotations to a POJO, and they ask why they can't make MBeans the same way. In version 2.0 of the JMX API, being defined by JSR 255, this will be possible.

The exact details are still subject to change as a result of discussions within the JSR 255 Expert Group, but here's a snapshot of where we are now. I think the final version will be fairly close to this.

In addition to defining MBeans with annotations, there are some new proposed annotations that will also apply to MBeans defined in the existing ways.

Prior art

Several projects already exist that provide this functionality, but the most developed is probably Spring. So our starting point was Spring's MBean annotations (see also API documentation).

Defining an MBean

In the proposed design, an MBean can be defined through annotations to achieve the same effect as a Standard MBean. In this and other examples, I'll show what you write with today's API, and what you'll be able to write with the new annotations.

TodayTomorrow
public interface CacheMBean {
    public int getSize();
    public void setSize(int size);

    public int getUsed();

    public int dropOldest(int n);
}

public class Cache implements CacheMBean {

    public int getSize() {...}

    public void setSize(int size) {...}


    public int getUsed() {...}


    public int dropOldest(int n) {...}
}
 







@MBean
public class Cache {
    @ManagedAttribute
    public int getSize() {...}
    @ManagedAttribute
    public void setSize(int size) {...}

    @ManagedAttribute
    public int getUsed() {...}

    @ManagedOperation
    public int dropOldest(int n) {...}
}

This defines an MBean with read-only attribute Used, read-write attribute Size, and operation dropOldest.

I'll call an MBean defined this way an @MBean.

One way to look at this is that with the existing Standard MBeans, public methods from the class are picked out as being management methods by virtue of being in the Standard MBean interface that the class implements. So in this example the CacheMBean interface defines which methods in Cache are the management methods. In the new form, the methods are picked out by being annotated, and there is no need to define an interface.

Pros and cons of @MBeans

The new style appears considerably more convenient than Standard MBeans. You only have to maintain one source file, rather than managing a class and an interface.

There is a downside, however, which may show up in bigger projects. The advantage of the Standard MBean approach is that the MBean interface tells you exactly what the attributes and operations of the MBean are. There is no extraneous information in the MBean interface: every method defines an attribute or an operation.

On the other hand, with @MBeans the management attributes and operations are potentially mixed in with many other methods, public or private. So it is not immediately obvious what the management interface of the MBean is.

This disadvantage applies both when reading the source code and when looking at the Javadoc output.

A second disadvantage is that it is no longer possible to construct a proxy. Proxies simplify client code by allowing it to access attributes and operations as compiler-checked method calls. They don't matter if you are only going to interact with your MBeans through a graphical interface like JConsole, but they are a big help if you are writing an application that will interact with your MBeans specifically.

For smallish projects, these disadvantages are likely to be minor. Furthermore, it should be possible to define an annotation processor that extracts a Standard MBean interface from an @MBean, so it can be used for documentation and proxying. In the example above, the annotation processor could create the CacheMBean interface every time you compile your program, based on the @ManagedAttribute and @ManagedOperation annotations in the Cache class.

Defining an MXBean

The existing @MXBean annotation can be used instead of @MBean to define an MXBean rather than a Standard MBean.

@MXBean
public class Cache {
    ...remainder as above...
}

Descriptions

Although the JMX API allows for textual descriptions to be associated with attributes, operations, and parameters, when you use a Standard MBean today these descriptions have meaningless default values. I've written before about how you can add meaningful descriptions, but it isn't easy. This is a really obvious use for annotations.

The proposed new @Description annotation can be used with Standard MBeans, MXBeans, and @MBeans. (Notice that both columns use the new API here!)

Tomorrow's Standard MBeanTomorrow's @MBean
@Description("some sort of cache")
public interface CacheMBean {

    @Description("number of cache slots in use")
    public int getUsed();
    ...
}

public class Cache implements CacheMBean {

    public int getUsed() {...}
    ...
}
@Description("some sort of cache")
public class Cache {
    @ManagedAttribute
    @Description("number of cache slots in use")
    public int getUsed() {...}
    ...
}






We international types will of course be thinking about internationalization, and I'll have more to say about that below.

Finding the MBeanServer and/or ObjectName

Often an MBean needs to know what MBean Server it is registered in, or what its name is in that MBean Server. To do this it currently needs to implement the MBeanRegistration callback interface. The required values are passed to that interface's preRegister method. But the interface contains three other methods, which the MBean must implement even if it has nothing interesting to do in them.

In the new proposal, the @Resource annotation from javax.annotation can be used instead of implementing MBeanRegistration when all that's needed is to discover what the MBeanServer or ObjectName is:

TodayTomorrow
 
public class Cache
    	implements CacheMBean, MBeanRegistration {

    private volatile MBeanServer mbs;

    private volatile ObjectName myName;

    public ObjectName preRegister(
    	    MBeanServer mbs, ObjectName name) {
    	this.mbs = mbs;
    	this.myName = name;
    	return name;
    }
    public void postRegister(Boolean done) {}
    public void preDeregister() {}
    public void postDeregister() {}

    ...
}
@MBean
public class Cache {

    @Resource
    private volatile MBeanServer mbs;
    @Resource
    private volatile ObjectName myName;











    ...
}

When the MBean is registered, the MBean Server will inject the appropriate values into these fields.

This possibility is open to all types of MBeans, not just @MBeans. You could continue to have a Standard MBean as today, but stop implementing MBeanRegistration in favour of @Resource annotations.

Simplified notification handling

Today, if an MBean emits notifications then it must implement the NotificationBroadcaster or NotificationEmitter interface. This means it must keep track of the set of listeners, as listeners are added and removed. It must also define the list of notification types that it can emit, by implementing getNotificationInfo().

In practice, everybody uses the NotificationBroadcasterSupport class instead of doing all this work themselves. In the simplest case, you just inherit from that class, and pass the list of notification types to the superclass constructor. If you already have a superclass, then you need to have a private NotificationBroadcasterSupport instance and delegate the NotificationBroadcaster methods to it.

New annotations allow you to define the list of notification types more simply, and to emit notifications without having to keep track of listeners.

TodayTomorrow
 


public class Cache
    	extends NotificationBroadcasterSupport
    	implements CacheMBean {
    public Cache() {
    	super(new MBeanNotificationInfo[] {
    	    new MBeanNotificationInfo(
    	    	new String[] {"my.notif.type"},
    	    	Notification.class.getName(),
    	    	"my notification"
    	    )}
    	);
    }

    ...
    void somethingHappened() {
    	Notification n = new Notification(...);
    	super.sendNotification(n);
    }
    ...
}
@MBean
@NotificationInfo(types={"my.notif.type"},
    description=@Description("my notification"))
public class Cache {


    @Resource
    private volatile SendNotification send;








    ...
    void somethingHappened() {
    	Notification n = new Notification(...);
        send.sendNotification(n);
    }
    ...
}

The @NotificationInfo annotation is what allows you to avoid constructing an MBeanNotificationInfo as in the messy "Today" code.

The new SendNotification interface contains just the method sendNotification. When you register this MBean in the MBean Server, it will inject an object into the send field which allows the MBean to send notifications. The MBean no longer has to be concerned with managing listeners, which happens somewhere behind the scenes.

Resource injection of SendNotification is available to all types of MBeans. Defining the notification types with @NotificationInfo is not available to Dynamic MBeans, which are expected to provide a complete MBeanInfo, including the MBeanNotificationInfo[] array.

More detail than you want to know about @NotificationInfo appears below.

You can stop reading now

If your eyes are already glazing over with all this code, you can safely stop here, and you'll have seen the main ideas. The remainder of this entry is about secondary items, and further details about the main ones.

Descriptor contents

In the JMX API included in the Java SE 6 platform, we introduced a way to define your own annotations to specify Descriptor contents. So you might define @Units like this:

@Documented @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Units {
    @DescriptorKey("units")
    String value();
}

The new API accepts such annotations on classes or methods that also have the @ManagedX annotations. For example:

@MBean
public class Cache {
    ...
    @Units("bytes")
    @ManagedAttribute
    public int getUsed() {...}
    ...
}

We've also added a new @DesriptorFields annotation, since the use of @DescriptorKey is somewhat non-obvious, and overkill for occasional use. So you can achieve the same effect like this:

@MBean
public class Cache {
    ...
    @DescriptorFields("units=bytes")
    @ManagedAttribute
    public int getUsed() {...}
    ...
}

This annotation can be used in Standard MBeans and MXBeans as well as @MBeans and @MXBeans.

Operation impact

The @ManagedOperation annotation has an optional element impact of type Impact. This is a new enum with values {INFO, ACTION, ACTION_INFO, UNKNOWN} corresponding to the integer codes defined by MBeanOperationInfo. So you can do this:

@MBean
public class Cache {
    ...
    @ManagedOperation(impact = Impact.ACTION)
    public int dropOldest(int n) {...}
}

You can also apply @ManagedOperation to a method in a Standard MBean interface or an MXBean interface in order to specify the impact.

MBean constructors

Each public constructor in an @MBean or @MXBean is converted into an MBeanConstructorInfo inside the MBean's MBeanInfo. This is exactly the same as for existing Standard MBeans and MXBeans.

StandardMBean class

The class StandardMBean can be used to customize an @MBean or an @MXBean in the same way as for a Standard MBean or MXBean today. You simply supply null for the mbeanInterface parameter in the constructor.

Details on Resource injection

The @Resource annotation can be used to inject an ObjectName, MBeanServer, or SendNotification. The annotation can be applied to a field or to a void method with a single parameter. For example:

    @Resource  // field injection
    private volatile ObjectName name;

    private MBeanServer mbs;
    @Resource  // method injection
    private synchronized void setMBeanServer(MBeanServer mbs) {
        this.mbs = mbs;
    }

The MBean Server determines what to inject based on the type. The type is either the declared type of the field or parameter, or it is specified explicitly in the @Resource annotation. For example, the following annotations have the same effect:

    @Resource
    private volatile ObjectName name;

    @Resource(type = ObjectName.class)
    private volatile Object name;

I don't think the second form will be used very often, but it might be used to inject the MBeanServer into a field of type MBeanServerConnection, for example.

The ObjectName (etc) will be injected as many times as there are appropriate @Resource annotations, including in parent classes.

@Resource annotations that don't match one of the given types are ignored. (Perhaps they are for some other API.) But even if the type is not recognized, @Resource fields and methods must be instance (not static), and @Resource methods must have exactly one parameter and return void.

I've used volatile in all these examples because the Java Memory Model would not otherwise guarantee that the MBean would actually see the injected values. For method-based injection, synchronized is an alternative, provided the MBean also uses synchronized to access the injected value. Notice that the same considerations apply to the existing MBeanRegistration technique.

Resource injection happens after the MBean's preRegister method (if any) is called, but before the MBean is registered in the MBean Server. If an injection method throws an exception, then postRegister(false) will be called and the exception will be thrown in the same way as for preRegister.

More on descriptions

In addition to the description text, the @Description annotation can specify the values of the descriptionResourceBundleBaseName and descriptionResourceKey fields in the corresponding Descriptor. This is enough to allow for internationalization:

@Description(value="some sort of cache",
             key="cache.mbean.description",
             bundleBaseName="MyResources")

To complete the story here, we need to have something that is able to apply these Descriptor fields to localize the MBeanInfo. We have some ideas on what that something might look like, but they are not yet fully formed.

More on @NotificationInfo

As I threatened, here is more information than you wanted to know about @NotificationInfo.

If an MBean has a @NotificationInfo annotation, then that annotation is translated into an MBeanNotificationInfo in the MBean's MBeanInfo. MBeanNotificationInfo includes a name which is the name of the notification class. It is usually "javax.management.Notification", but it might be a subclass. So @NotificationInfo has an optional notificationClass element which is a Class<? extends Notification>. For example:

@NotificationInfo(types = {AttributeChangeNotification.ATTRIBUTE_CHANGE},
                  notificationClass = AttributeChangeNotification.class)
    

If the MBean can emit more than one class of MBean, then it can use @NotificationInfos:

@NotificationInfos(
    @NotificationInfo(types = {"my.first.notif", "my.second.notif"})
    @NotificationInfo(types = {AttributeChangeNotification.ATTRIBUTE_CHANGE},
                      notificationClass = AttributeChangeNotification.class)
)

The @NotificationInfo is applied to an MBean class, but a @Description on that class applies to the MBean, not its notifications. The existence of @NotificationInfos is another reason why we cannot use @Description straightforwardly.

This is why there is an optional element of type @Description inside @NotificationInfo, so you would write:

@NotificationInfo(types={"my.notif.type"},
      description=@Description(value="my notification", key="my.notif.descr"))

You cannot use @DescriptorFields, for the same reason as @Description, so there's another optional element that allows you to write:

@NotificationInfo(types={"my.notif.type"},
      descriptorFields={"foo=bar"})

Ideas still in progress

We're studying the possibility of providing a way to cause a notification that is sent every time a given operation is completed.

We're looking at ways in which an MBean could say what its ObjectName is. Probably the MBean would only provide a subset of the information needed to construct the name. It's still unclear exactly what this might look like.

What next?

This is still work in progress, as you'll have gathered. I'm very much interested in comments and suggestions, either here or at jmx-spec-comments@sun.com. Thanks!

[Tags: ]



When can JMX notifications be lost?

Posted by emcmanus on August 23, 2007 at 12:32 AM | Permalink | Comments (6)

The JMX Best Practices guide says notifications can sometimes be lost. Why is that? When might it happen? Read on.

Here's the relevant text from the Best Practices guide:

It is important to be aware of the semantics of notification delivery when defining how notifications are used in a model. Remote clients cannot assume that they will receive all notifications for which they are listening. The JMX Remote API only guarantees a weaker condition:

A client either receives all notifications for which it is listening, or can discover that notifications may have been lost.

This text might seem somewhat alarming. First of all, notice that it only applies to remote clients. A local client (within the same Java VM) will reliably get all notifications it asks for.

Secondly, the text is describing something that will only happen in unusual circumstances. Notifications will only be lost when they arrive so fast that they cannot be delivered to the remote client quickly enough, or if there is a long network outage during which enough notifications arrive to overflow the notification buffer on the server. If you're sure that the rate of notifications is always low then you probably don't need to worry. Long network outages will probably trigger other problems in your client, so you'll need to deal with them more generally than just worrying about lost notifications.

Careful clients

But if you have many notifications, you probably want to follow the advice in the subsequent paragraphs of the Best Practices guide:

Notifications should never be used to deliver information that is not also available in another way. The typical client observes the initial state of the information model, then reacts to changes in the model signalled by notifications. If it sees that notifications may have been lost, it goes back and observes the state of the model again using the same logic as it used initially. The information model must be designed so that this is always possible. Losing a notification must not mean losing information irretrievably.

When a notification signals an event that might require intervention from the client, the client should be able to retrieve the information needed to react. This might be an attribute in an MBean that contains the same information as was included in the notification. If the information is just that a certain event occurred, it is often enough just to have a counter of how many times it occurred. Then a client can detect that the event occurred just by seeing that the counter has changed.

Stateless servers

The design of the existing standard connectors is such that notification loss can happen when there are many notifications coming from the MBeans in the MBean Server. This is true even for clients that are only listening for a small subset of those notifications. In the extreme case, a client that is listening for a very rare notification might not see it, because other MBeans are generating frequent notifications that nobody is listening to. Once again, the client can tell that this has happened (via JMXConnector.addConnectionNotificationListener).

The existing connectors behave like this because they have been designed to have no non-transient state on the server. A consequence is that the server has no non-transient record of which clients are interested in which notifications. Therefore it has to store all notifications in its buffer, in case some client it doesn't remember is interested in them.

The servers were designed to have no non-transient state for better scalability. In retrospect, this was probably a design mistake. In many client/server systems, you have one server, or just a few servers, and a large number of clients. So limiting state in the server is an excellent idea, because it allows the server to handle many more clients. But in management systems, the situation is usually the opposite: you typically have one client (a management program such as JConsole) that may connect to and manage many servers. There are no common use cases where a server might have a large number of JMX clients.

In version 2.0 of the JMX API, being defined by JSR 255, we are adding an Event Service. Among other things, this will fix the problem where a client might lose notifications that it is interested in because there are many other notifications that it is not interested in.

Notification loss is inevitable

Even with the new Event Service, notification loss will still be possible, however. Can't we get rid of it?

To answer this question, consider what happens when notifications are produced faster than they can be handled. This might be because of network delays, or because the client needs to do some work for each notification. Suppose this situation persists. What should the system do?

There are basically three possibilities:

  1. Some notifications are eventually dropped. This is what the JMX Remote API does, and it is also what the new Event Service will do.
  2. Notification senders are slowed down. This is what usually happens in the local case. An MBean sends a notification to a local listener by invoking the listener's handleNotification method. Unless it has multiple threads, the MBean will wait for that method to complete before doing anything else, including sending any more notifications.
  3. Notifications accumulate in an unbounded buffer. This is actually the worst solution. In the real world there is no such thing as an unbounded buffer. And even if you save the notifications in a giant disk, which is effectively unbounded, you still haven't fixed the problem that the client is getting further and further behind the server. When the client finally gets a notification that was sent yesterday, is that still any use?

When we were designing the JMX Remote API, we assumed that most MBeans that send notifications were not expecting sending to be slow. In the local case, sending is just invoking a method, and that method is usually punctual. If we had wanted to apply solution 2, slowing down senders, that could have broken the assumptions of existing MBeans. Coding MBeans so that they can cope with a blocked send would also be considerably more difficult. So, even though this solution (flow control) is arguably better, we were reluctant to impose it.

The future: JMX Event Service

As I mentioned, in version 2.0 of the JMX API we are designing a new Event Service. This will be part of the JDK 7 platform. Though it will not eliminate notification loss, it will significantly reduce the likelihood of such loss. And it will also allow you to plug in your own transport for notifications. In particular you could plug in the Java Message Service to use an existing message bus.

[Tags: .]

Combining Cascading with the Attach API

Posted by emcmanus on August 01, 2007 at 03:09 AM | Permalink | Comments (7)

The Attach API lets you discover and attach to the Java VMs running on your local machine. JMX Cascading lets you federate several JMX agents together. Can we combine the two?

This is a question we've had fairly often, and I was prompted to write about it after Nilesh Bansal posted a question to the JMX forum in the Sun Developer Network to which one possible answer is to combine Cascading with the Attach API.

The solution I propose here assumes that you are running on JDK 6. If you are stuck on JDK 5, I have a few suggestions below, but there is inevitably a loss of functionality.

The basic idea

The Attach API is what JConsole uses to list the local Java processes in its Connect dialog. It is a nonstandard but supported API, which means it should be present on any JDK-derived VM that is at least version 6.

Once you select a Java process to connect to, JConsole again uses the Attach API to find the address of the JMX agent within that process, so it can connect. And, thanks to some deep magic, JConsole can use the Attach API to create a JMX agent within the process if it doesn't already have one.

So if we grab the code out of JConsole that does all this, we can combine it with Cascading. The picture below is my standard Cascading picture. Here, we're going to create a Master Agent that you can use to see the MBeans in all of your local Java processes. The Subagents in the picture are these local processes, and all of their MBeans are imported into the Master Agent. So if you connect JConsole to the Master Agent, then you can also see what's going on in each of the Subagents. In other words, you can use one JConsole connection to see all of your Java processes on that machine.

MBeans from every JVM on a machine are imported into a Master Agent

If you set up the Master Agent to be remotely connectable, then you can also see all of your processes from a JConsole running on another machine. The easiest way to do this is with -Dcom.sun.management.jmxremote.port and related properties. You need to pay attention to security in this case, of course. You probably don't want just anyone to be able to see and modify the MBeans of your processes.

Here's what we'd like JConsole to look like when it connects to the Master Agent:

JConsole attached to Master Agent shows MBeans from every JVM

In the picture, you can see cascaded MBeans from processes 16872, 18213, and 29375. Through the "Cascader" MBean, which I'll define below, you can see what command each process id corresponds to. 29375 is the command com.sun.enterprise.server.PELaunch, which is the Sun App Server. I launched that because it has a non-trivial number of MBeans, as you can divine from the MBean domains on the left.

The processes you see are your own processes. In other words they are the ones that you are able to manipulate if you are logged in to the machine. On Unix machines, this means they are processes that are running with the same user id as the Master Agent. On Windows machines, it means processes that you have the necessary privileges to open and interact with.

The details

I'll use the Cascading implementation from Open DMK, the Open Source version of the Java Dynamic Management Kit (Java DMK) product. (When I wrote my earlier blog entry about cascading, Open DMK hadn't yet been released.)

So to try this out, you'll need to download the Open DMK binaries. You'll need to compile and run with OpenDMK-bin/lib/jdmkrt.jar from the Binary Zip. The Binary Zip also includes API documentation if you want to see the details of the API I'm using.

You'll also need to compile and run with tools.jar from the JDK to get the Attach API. It's in the lib directory of your JDK 6 installation.

The program creates a "Cascader" MBean, which has one operation and one attribute. The refresh operation gets the current list of Java processes and sets up cascading to import from each one. I don't attempt to detect when new processes are created, though it would be straightforward to call refresh periodically in a loop. When a process exits, the Master Agent's connection to it will break, and Cascading will automatically remove the MBeans that were imported from that process.

The CascadeResults attribute shows the result of attaching to each Java process - either an exception message, or a success message including the process's command-line parameters. In the latter case you can use this to figure out what the process is. That's how I knew that 29375 was the App Server, above.

(A bug in JConsole makes it hard to see the command-line when it is long. If you let the mouse hover over the value, you can see a longer string.)

Since the full code is fairly involved, here's a sketch of how I discover the Java processes and set up cascading for them. It omits exception handling and the like.

List<VirtualMachineDescriptor> vmds = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : vmds) {

    String id = vmd.id();
    // This is the id that we will prefix to MBean names.  In practice
    // it is a numeric process id, as we have seen.

    VirtualMachine vm = VirtualMachine.attach(vmd);

    Properties agentProps = vm.getAgentProperties();

    String connectorAddressProperty =
    	"com.sun.management.jmxremote.localConnectorAddress";
    if (!agentProps.containsKey(connectorAddressProperty)) {
    	// The above property will be present if the process was launched
    	// with -Dcom.sun.management.jmxremote or if this program or
    	// JConsole previously attached to it.  Otherwise we need to start
    	// the management agent within the process:

    	String javaHome = vm.getSystemProperties().getProperty("java.home");
    	String agent = javaHome + "/lib/management-agent.jar";

    	vm.loadAgent(agent, "com.sun.management.jmxremote");
    	// This has the same effect as running with -Dcom.sun.management.jmxremote

    	agentProps = vm.getAgentProperties();
    }

    String connectorAddress = agentProps.getProperty(connectorAddressProperty);
    JMXServiceURL url = new JMXServiceURL(connectorAddress);

    CascadingService cascade = ...;
    cascade.mount(url, connectOptions, objectNamePattern, id);

    vm.detach();
}

The complete code is below. You'll probably want to customize it, for example so it only cascades from certain of your processes rather than all of them.

Cascade loops

One detail I omitted from the sketch is that we have to be careful that the Master Agent doesn't try to import its own MBeans. Say the Master Agent is process 12345. It will show up in the list of Java processes from the Attach API, along with all the others. If we handled it the same way as the others, then the MBean java.lang:type=Runtime, for example, would be imported as 12345/java.lang:type=Runtime. Cascading detects when new MBeans are created, and imports them. So it would detect the new 12345/java.lang:type=Runtime and import it as 12345/12345/java.lang:type=Runtime. And so on.

The way I prevent this is by checking the MBeanServerId property of the MBean Server Delegate in each subagent. If it is the same as the MBeanServerId of the Master Agent, then we know that the supposed subagent is actually the Master Agent.

A more efficient technique would be to compare the VM id against the RuntimeMXBean's Name attribute in the Master Agent. But that would require making assumptions that go beyond what is strictly specified.

Stuck on JDK 5?

If you're currently on JDK 5, the Attach API is one of the many goodies that may encourage you to migrate to JDK 6. But what if you can't?

In that case, I think your best bet is to look at the JConsole code from the JDK 5 sources, and try to do the same thing it does. The relevant code is in the method getManagedVirtualMachines() within the class sun.tools.jconsole.ConnectDialog. This solution is fragile, because nothing prevents an update of JDK 5 from changing how this works and breaking your code. But if you're prepared to live with that, this is a possible approach.

Since JDK 5 doesn't have Attach On Demand, you will only be able to cascade processes that were explicitly run with the -Dcom.sun.management.jmxremote option. I don't see any way around that.

The code

package net.mcmanus.eamonn.cascadelocalvms;

import com.sun.jdmk.remote.cascading.CascadingService;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerDelegate;
import javax.management.MBeanServerDelegateMBean;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

public class Cascade {
    private static final String localConnectorAddressProperty =
            "com.sun.management.jmxremote.localConnectorAddress";
    private static final Logger logger = Logger.getLogger(Cascade.class.getName());

    public static void main(String[] args) throws Exception {
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();

        CascadingService cascade = new CascadingService(mbs);

        CascaderImpl cascader = new CascaderImpl(cascade);
        cascader.refresh();
        mbs.registerMBean(cascader, new ObjectName(":type=Cascader"));

        System.out.println("Ready");

        Thread.sleep(Long.MAX_VALUE);
        // If this thread exits then the app will exit, so don't let it.
    }

    public static interface CascaderMXBean {
        public void refresh();
        public SortedMap<String, String> getCascadeResults();
        // key in map is VM id (typically a pid)
        // value is result of trying to cascade from that VM
    }

    public static class CascaderImpl implements CascaderMXBean {
        private final CascadingService cascade;
        private SortedMap<String, String> cascadeResults;

        CascaderImpl(CascadingService cascade) {
            this.cascade = cascade;
        }

        public void refresh() {
            uncascadeJVMs(cascade);
            cascadeResults = cascadeJVMs(cascade);
        }

        public SortedMap<String, String> getCascadeResults() {
            return cascadeResults;
        }
    }

    private static SortedMap<String, String> cascadeJVMs(CascadingService cascade) {
        logger.fine("Cascading JVMs...");

        SortedMap<String, String> results = new TreeMap<String, String>();

        List<VirtualMachineDescriptor> vmds = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : vmds) {
            logger.fine("Attaching to " + vmd);
            String id = vmd.id();
            VirtualMachine vm;
            try {
                vm = VirtualMachine.attach(vmd);
            } catch (Exception e) {
                logger.log(Level.FINE, "...exception attaching", e);
                results.put(id, "Exception attaching: " + e);
                continue;
            }

            // From this point on we must detach, so we need a 'finally' block
            try {
                Properties agentProps = vm.getAgentProperties();
                if (!agentProps.containsKey(localConnectorAddressProperty)) {
                    logger.fine("...loading management agent into JVM");
                    try {
                        loadManagementAgent(vm);
                    } catch (Exception e) {
                        logger.log(Level.FINE, "...exception loading agent", e);
                        results.put(id, "Exception loading agent: " + e);
                        continue;
                    }
                    agentProps = vm.getAgentProperties();
                }

                String addr = agentProps.getProperty(localConnectorAddressProperty);
                if (addr == null) {
                    logger.fine("...still don't have connector address??");
                    results.put(id, "No connector address even after loading agent");
                    continue;
                }

                JMXServiceURL url = new JMXServiceURL(addr);
                logger.finer("...connector address is " + url);

                if (isThisJVM(url, cascade.getTargetMBeanServer())) {
                    logger.fine("...is local JVM");
                    results.put(id, "Local JVM");
                    continue;
                }

                Map<String, ?> connectOptions = null;
                ObjectName pattern = new ObjectName("*:*");
                String mountTo = id;

                try {
                    String mountId =
                            cascade.mount(url, connectOptions, pattern, mountTo);
                    logger.log(Level.FINE, "...mounted with id " + mountId);
                    results.put(id, "Success: " + vmd.displayName());
                } catch (Exception e) {
                    logger.log(Level.FINE, "...exception mounting JVM", e);
                    results.put(id, "Exception mounting JVM: " + e);
                }

            } catch (Exception e) {
                logger.log(Level.FINE, "...unexpected exception", e);
                results.put(id, "Unexpected exception: " + e);
            } finally {
                try {
                    vm.detach();
                } catch (IOException e) {
                    logger.log(Level.INFO, "Could not detach vm " + id, e);
                }
            }
        }

        return results;
    }

    private static void uncascadeJVMs(CascadingService cascade) {
        String[] ids = cascade.getMountPointIDs();
        for (String id : ids) {
            try {
                boolean unmounted = cascade.unmount(id);
                if (!unmounted)
                    logger.info("Was not mounted: " + id);
            } catch (IOException e) {
                logger.log(Level.FINE, "Exception unmounting " + id, e);
            }
        }
    }

    private static void loadManagementAgent(VirtualMachine vm)
    throws AgentLoadException, AgentInitializationException, IOException {
        String javaHome = vm.getSystemProperties().getProperty("java.home");
        String agent = javaHome + File.separator +
                "lib" + File.separator + "management-agent.jar";
        File f = new File(agent);
        if (!f.exists())
            throw new IOException("Management agent not found: " + agent);
        agent = f.getCanonicalPath();
        logger.fine("...load management agent from " + agent);
        vm.loadAgent(agent, "com.sun.management.jmxremote");
    }

    private static boolean isThisJVM(JMXServiceURL url, MBeanServer mbs) {
        try {
            JMXConnector jmxc = JMXConnectorFactory.connect(url);
            MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
            MBeanServerDelegateMBean myDelegate =
                JMX.newMBeanProxy(mbs, MBeanServerDelegate.DELEGATE_NAME,
                    MBeanServerDelegateMBean.class);
            MBeanServerDelegateMBean remoteDelegate =
                JMX.newMBeanProxy(mbsc, MBeanServerDelegate.DELEGATE_NAME,
                    MBeanServerDelegateMBean.class);
            boolean same = myDelegate.getMBeanServerId().equals(
                    remoteDelegate.getMBeanServerId());
            jmxc.close();
            return same;
        } catch (Exception e) {
            logger.log(Level.FINE, "Cannot determine if same JVM", e);
            return true;  // if it really IS the same JVM, then we might
                          // get an infinite cascading loop
        }
    }
}
// END OF CODE

[Tags: , jdmk, .]



"Top threads" plugin for JConsole

Posted by emcmanus on June 21, 2007 at 08:14 AM | Permalink | Comments (1)

Peter Doornbosch has created a much improved version of the JTop sample plugin for JConsole. JTop only shows thread name, cumulative CPU time, and thread state. The "top threads" plugin also shows, per thread, current CPU percentage, average CPU percentage, and the evolution of CPU percentage over time. Very nice!

There does seem to be a bug whereby the TextArea that shows the stack trace of the selected thread is editable. Furthermore this TextArea is "always on top" so it can obscure part of the "new connection" dialogue. I found that I could shrink the TextArea down to nothing by grabbing the control at its top, so this problem isn't very severe.

Thanks to Daniel Fuchs for the pointer.

[Tags: jconsole jtop.]



Disassembling serialized Java objects

Posted by emcmanus on June 12, 2007 at 06:51 AM | Permalink | Comments (5)

Presenting Serialysis, a library that allows you to disassemble the serial form of Java objects. This can allow you to retrieve information about an object that is not available through its public API. It is also a useful tool when testing the serialization of your classes.

When the public API is not enough

My reason for writing this library is that I encountered a couple of problems where I found that I needed information from an object that was not available through its public API, but that was available through its serial form.

One example is if you have a stub for a remote RMI object, and you want to know what address it will connect to, or what port, or using what socket factory. The standard RMI API doesn't give you any way to extract this information from the stub. But the information is there, and it must be included when the stub is serialized so that the stub is usable when it is later deserialized. So if we could somehow parse the serialized stub we could get the information we want.

A second example comes from the JMX API. Queries to the MBean Server are represented by the interface QueryExp. QueryExp instances are constructed using the methods of the Query class. If you have an object implementing QueryExp, how can you know what query it executes? The JMX API doesn't include any method to find out. The information must be present in the serial form, so that when a client sends a query to a remote server it can be reconstituted on the server. If we could look at the serial form, we could find out what the query was.

This second example is what prompted me to write this library. The existing standard JMX connectors are based on Java serialization, so they don't need to do anything special for QueryExps. But the new Web Services Connector being defined by JSR 262 uses XML for serialization. How can it analyze a QueryExp in order to convert it into XML? The answer is that the WS Connector uses a version of this library to look at the Java-serialized QueryExp.

What these examples have in common is that they illustrate gaps in the relevant APIs. There ought to be methods that allow you to extract the information contained in an RMI stub. There ought to be methods that convert back from a QueryExp object to the original Query methods that constructed it. (Even a standardized parseable toString() would be enough.) But those methods aren't there today, and if we want code that works with those APIs as they are now, we need another approach.

Grabbing the private fields of objects

If you have the source code of the classes you're interested in, it's tempting just to barrel in and grab the information you need. In the RMI stub example, we can find out by experiment that the stub's getRef() method returns a sun.rmi.server.UnicastRef, and by studying the JDK source we might be able to figure out that this class contains a field ref of type sun.rmi.transport.LiveRef with the information we need. So we might end up with code like this:

// This is NOT a good idea!!!

import sun.rmi.server.*;
import sun.rmi.transport.*;
import java.rmi.*;
import java.rmi.server.*;

public class StubDigger {
    public static getPort(RemoteStub stub) throws Exception {
        RemoteRef ref = stub.getRef();
    	UnicastRef uref = (UnicastRef) ref;
    	Field refField = UnicastRef.class.getDeclaredField("ref");
    	refField.setAccessible(true);
    	LiveRef lref = (LiveRef) refField.get(uref);
    	return lref.getPort();
    }
}

You might be satisfied with this, but you shouldn't be. The code in bold is full of horrors. First of all, you should never depend on sun.* classes, because there's no guarantee they won't change unrecognizably in any JDK update, plus of course your code probably won't be portable to platforms other than the JDK. Secondly, it's a huge red flag when you see Field.setAccessible being called. That means the code is depending on undocumented fields, which again could change between releases, or, worse, which might continue to exist but with subtly different semantics.

(The above code was written for JDK 5. It turns out that in JDK 6, LiveRef acquires a public getPort() method, so you no longer need Field.setAccessible. But you still need to depend on sun.* classes.)

Well, sometimes you can't do any better than this. But if the class you're interested in is serializable, often you can. The reason is that the serial form of a class is part of its public interface. If the API is any good at all then its public interfaces will evolve compatibly in every update. This is a very strong requirement on the JDK platform in particular.

So if the information you need isn't available through a class's public methods, but is part of the documented serial form, then you can rely on it remaining in the serial form in the future.

The serial form is included in the Javadoc output as part of the See Also for each serializable class. You can see the serial form of all public JDK classes in a single giant page.

Enter Serialysis

My library to parse serialized objects is called Serialysis, the result of cramming the words "serial analysis" too close together.

Here's a simple example of what it looks like in action. This code...

    	SEntity sint = SerialScan.examine(new Integer(5));
	System.out.println(sint);

...produces this output...

SObject(java.lang.Integer){
  value = Prim(int){5}
}

This tells us that the java.lang.Integer that we gave to SerialScan.examine serializes as an object with a single field value of type int. If we check out the documented serialized form of java.lang.Integer we can see that this is indeed what is expected.

If you check out the source code of java.lang.Integer, you'll see that the class itself also has a single field value of type int:

    /**
     * The value of the <code>Integer</code>.
     *
     * @serial
     */
    private final int value;

But private fields are an implementation detail. An update could rename this field, or replace it with a new field inherited from the parent class java.lang.Number, or whatever. There's no guarantee that that won't happen, but there is a guarantee that the serial form will remain the same. Serialization provides mechanisms to keep the serial form the same even when the class's fields change.

Here's a more complicated example. Suppose that, for some reason, we want to know how big the array in an ArrayList is. The API doesn't allow us to find out, though it does allow us to force the array to be at least a certain size.

If we check the serial form of ArrayList, we see that it does contain the information we're looking for. There's a serialized field size, which is the number of elements in the list. That's not what we want. But the Serial Data in the writeObject method does have what we want:

Serial Data:
The length of the array backing the ArrayList instance is emitted (int), followed by all of its elements (each an Object) in the proper order.

If we execute this code...

	List<Integer> list = new ArrayList<Integer>();
	list.add(5);
	SObject slist = (SObject) SerialScan.examine(list);
	System.out.println(slist);

...we get this output...

SObject(java.util.ArrayList){
  size = SPrim(int){1}
  -- data written by class's writeObject:
  SBlockData(blockdata){4 bytes of binary data}
  SObject(java.lang.Integer){
    value = SPrim(int){5}
  }
}

This is where we get into the gory details of serialization. In addition to, or instead of, serializing an object's fields, its class can declare a method writeObject(ObjectOutputStream) that writes arbitrary data to the serial stream using methods like ObjectOutputStream.writeInt. It must declare a corresponding readObject that reads the same data, and it should document via a @serialData tag what the writeObject method writes, as ArrayList does.

The writeObject data is accessible in Serialysis through the method SObject.getAnnotations(), which returns a List<SEntity>. Each Object that was written via the method ObjectOutputStream.writeObject(Object) appears as an SObject in this list. Each chunk of data written by one or more consecutive calls to the methods that ObjectOutputStream gets from DataOutput (writeInt, writeUTF, etc) appears as an SBlockData. The serial stream doesn't include enough information to separate out individual items within the chunk; that information is an agreement between writer and reader that is documented by the @serialData tag.

Based on the ArrayList documentation, we can find the size of the array like this:

	SObject slist = (SObject) SerialScan.examine(list);
	List<SEntity> writeObjectData = slist.getAnnotations();
	SBlockData data = (SBlockData) writeObjectData.get(0);
	DataInputStream din = data.getDataInputStream();
	int alen = din.readInt();
	System.out.println("Array length: " + alen);

How Serialysis solves my example problems

Without showing all the details of the code, here's the outline of the solution to the QueryExp problem I mentioned. Suppose I have a QueryExp constructed like this:

QueryExp query =
    Query.or(Query.gt(Query.attr("Version"), Query.value(5)),
	     Query.eq(Query.attr("SupportsSpume"), Query.value(true)));

This means, "MBeans where the Version attribute is greater than 5 or the SupportsSpume attribute is true. The toString() of this query in the JDK looks like this:

((Version) > (5)) or ((SupportsSpume) = (true))

The result of SerialScan.examine looks like this:

SObject(javax.management.OrQueryExp){
  exp1 = SObject(javax.management.BinaryRelQueryExp){
    relOp = SPrim(int){0}
    exp1 = SObject(javax.management.AttributeValueExp){
      attr = SString(String){"version"}
    }
    exp2 = SObject(javax.management.NumericValueExp){
      val = SObject(java.lang.Long){
        value = SPrim(long){5}
      }
    }
  }
  exp2 = SObject(javax.management.BinaryRelQueryExp){
    relOp = SPrim(int){4}
    exp1 = SObject(javax.management.AttributeValueExp){
      attr = SString(String){"supportsSpume"}
    }
    exp2 = SObject(javax.management.BooleanValueExp){
      val = SPrim(boolean){true}
    }
  }
}

You can imagine code that descends into this structure producing an XML equivalent. Every conformant implementation of the JMX API is required to produce this same serial form, so the code that parses it is guaranteed to work everywhere.

Now here's the code that solves the RMI stub port number problem:

    public static int getPort(RemoteStub stub) throws IOException {
	SObject sstub = (SObject) SerialScan.examine(stub);
	List<SEntity> writeObjectData = sstub.getAnnotations();
	SBlockData sdata = (SBlockData) writeObjectData.get(0);
	DataInputStream din = sdata.getDataInputStream();
	String type = din.readUTF();
	if (type.equals("UnicastRef"))
	    return getPortUnicastRef(din);
	else if (type.equals("UnicastRef2"))
	    return getPortUnicastRef2(din);
	else
	    throw new IOException("Can't handle ref type " + type);
    }

    private static int getPortUnicastRef(DataInputStream din) throws IOException {
	String host = din.readUTF();
	return din.readInt();
    }

    private static int getPortUnicastRef2(DataInputStream din) throws IOException {
	byte hasCSF = din.readByte();
	String host = din.readUTF();
	return din.readInt();
    }

To understand this, you need to see the serial form for RemoteObject. This code is admittedly difficult, but it is portable and futureproof. It should be fairly clear how to extract the other information I mentioned from RMI stubs using the same approach.

Conclusions

You really don't want to get into disassembling serial forms unless you have to. But if you do have to, then Serialysis should make your task a little less painful.

It's also a good way to check that your own classes serialize the way you expect them to.

Download

You can download the Serialysis library at http://weblogs.java.net/blog/emcmanus/serialysis.zip.

[Tags: , , .]



Custom types for MXBeans

Posted by emcmanus on May 30, 2007 at 07:00 AM | Permalink | Comments (0)

MXBeans map between arbitrary Java types and a fixed set of types in javax.management.openmbean called the Open Types. This allows clients to interact with MXBeans, without needing to know the original Java types (which might require putting extra jars in their classpath and so on).

Up until now the mapping rules were fixed. Certain types can not be mapped by these rules, for example self-referential types or types such as Object or Number. In the Java 7 platform, we're planning to allow customization of the rules, as part of JSR 255 which is defining version 2.0 of the JMX API. This is a summary of the proposed changes.

What problem are we solving?

There are three sorts of use cases here:

  1. You have some types that you control and that are used in your MXBean interfaces. For example, java.lang.management.MemoryUsage which is referenced by java.lang.management.MemoryMXBean. An update to java.lang.management could update MemoryUsage, for example to add an annotation to it.
  2. You have some types that you do not control that are used in MXBean interfaces that you do control. For example, you are an end-user and you define an MXBean interface MyMemoryMXBean that references java.lang.management.MemoryUsage. You can't change MemoryUsage but you can change MyMemoryMXBean.
  3. You have some types that you do not control that are used in MXBean interfaces that you do not control either. For example, you want to define an MXBean-ified version of some legacy MBeans defined by somebody else.

The proposal is to add one class, one interface, and two annotations to address these cases. The existing class javax.management.StandardMBean acquires two new constructors and the existing class javax.management.JMX acquires two new overloads of existing methods.

MXBeanMapping and @MXBeanMappingClass

The most important change is the new class javax.management.openmbean.MXBeanMapping:

public abstract class MXBeanMapping {
    protected MXBeanMapping(Type javaType, OpenType<?> openType);
    public final Type getJavaType();
    public final OpenType<?> getOpenType();
    public final Class<?> getOpenClass();
    public abstract Object fromOpenValue(Object openValue) throws InvalidObjectException;
    public abstract Object toOpenValue(Object javaValue) throws OpenDataException;
    public void checkReconstructible() throws InvalidObjectException;
}

Suppose we want to define a mapping for the class MyLinkedList, which looks like this:

public class MyLinkedList {
    public MyLinkedList(String name, MyLinkedList next) {...}
    public String getName() {...}
    public MyLinkedList getNext() {...}
}

This is not a valid type for MXBeans, because it contains a self-referential property "next" defined by the getNext() method. (This example comes from Simone Bordet.) So we would like to specify a mapping for it explicitly. When an MXBean interface contains MyLinkedList, that will be mapped into a String[], which is a valid Open Type.

To define this mapping, we first subclass MXBeanMapping:

public class MyLinkedListMapping extends MXBeanMapping {
    public MyLinkedListMapping(Type type) throws OpenDataException {
        super(MyLinkedList.class, ArrayType.getArrayType(SimpleType.STRING));
        if (type != MyLinkedList.class)
            throw new OpenDataException("Mapping only valid for MyLinkedList");
    }

    @Override
    public Object fromOpenValue(Object openValue) throws InvalidObjectException {
        String[] array = (String[]) openValue;
        MyLinkedList list = null;
        for (int i = array.length - 1; i >= 0; i--)
            list = new MyLinkedList(array[i], list);
        return list;
    }

    @Override
    public Object toOpenValue(Object javaValue) throws OpenDataException {
        ArrayList<String> array = new ArrayList<String>();
        for (MyLinkedList list = (MyLinkedList) javaValue; list != null; list = list.getNext())
            array.add(list.getName());
        return array.toArray(new String[0]);
    }
}

The call to the superclass constructor specifies what the original Java type is (MyLinkedList.class) and what Open Type it is mapped to (ArrayType.getArrayType(SimpleType.STRING)). The fromOpenValue method says how we go from the Java type to the Open Type, and the toOpenValue method says how we go from the Open Type to the Java type.

With this mapping defined, we can annotate MyLinkedList with the new annotation @javax.management.openmbean.MXBeanMappingClass:

@MXBeanMappingClass(MyLinkedListMapping.class)
public class MyLinkedList {...}

Now we can use MyLinkedList in an MXBean interface and it will work. This satisfies use case 1 above.

MXBeanMappingFactory and @MXBeanMappingFactoryClass

If we are unable to annotate individual classes, then we can define a mapping factory that is consulted every time a type needs to be mapped. This is also useful if we would like to apply the same set of rules across a whole set of classes (for example, any class that implements List<E> is mapped in the same way as List<E>).

A mapping factory is a subclass of javax.management.openmbean.MXBeanMappingFactory:

public abstract class MXBeanMappingFactory {
    public static final MXBeanMappingFactory DEFAULT;
    protected MXBeanMappingFactory();
    public abstract MXBeanMapping mappingForType(Type t, MXBeanMappingFactory f)
    throws OpenDataException;
}

For example, suppose we can't change MyLinkedList, so we can't add the @MXBeanMappingClass annotation to it. We can achieve the same effect by defining a mapping factory as follows:

public class MyLinkedListMappingFactory extends MXBeanMappingFactory {
    public MyLinkedListMappingFactory() {}

    @Override
    public MXBeanMapping mappingForType(Type t, MXBeanMappingFactory f)
    throws OpenDataException {
        if (t == MyLinkedList.class)
            return new MyLinkedListMapping(t);
        else
            return MXBeanMappingFactory.DEFAULT.mappingForType(t, f);
    }
}

Now we can add the new annotation javax.management.openmbean.MXBeanMappingFactoryClass to any MXBean interface that references MyLinkedList:

@MXBeanMappingFactoryClass(MyLinkedListMappingFactory.class)
public interface SomethingMXBean {
    public MyLinkedList getSomething();
}

This satisfies use case 2 above.

New StandardMBean constructors and JMX.newMXBeanProxy method

The existing class StandardMBean is used to make instances of Standard MBeans and MXBeans when you need to control some aspects of their behaviour, such as the descriptions in the MBeanInfo. We can extend the set of constructors as follows:

public class StandardMBean implements DynamicMBean {
    ...existing constructors...
    public <T> StandardMBean(T impl, Class<T> intf, MBeanOptions options);
    protected StandardMBean(Class<?> intf, MBeanOptions options);
}

The options parameter allows us to specify a number of potentially interesting things:

  • an MXBeanMappingFactory (this is the case we're looking at here)
  • whether this is an MXBean or not
  • perhaps the contents of the MBeanInfo, as an alternative to subclassing and overriding e.g. getDescription(MBeanAttributeInfo)
  • whether the methods of MBeanRegistration should be forwarded to the implementation object if it implements that interface

The question of what the MBeanOptions type is is still open. It could be a Map<String, ?> where each option is represented by a string constant. This is the approach taken by the JMX Remote API, for example. Or, it could be a special-purpose class called MBeanOptions with methods to set each of the options. I plan to write more on this later; for now I'll assume it's the MBeanOptions option.

If you need to create an MXBean that implements the SomethingMXBean interface above and uses the MyLinkedListMappingFactory, but you can't add an annotation to SomethingMXBean, then you can do so using either of the two existing ways to use StandardMBean, subclassing or delegation, but supplying an option:

MBeanOptions options = MBeanOptions.DEFAULT.withMXBeanMappingFactory(
    new MyLinkedListMappingFactory);
// either use delegation:
SomethingMXBean impl = new SomethingImpl();
StandardMBean mbean = new StandardMBean(impl, SomethingMXBean.class, options);
// or use subclassing:
public class MySomething extends StandardMBean implements SomethingMXBean {
    public MySomething() {
        super(SomethingMXBean.class, options);
    }
}

If you have a custom mapping in your MBean server, then you need the same custom mapping in a client if the client is making a proxy. So we add another overloading of JMX.newMXBeanProxy:

public class JMX {
    ...
    public static <T> T newMXBeanProxy(
        MBeanServerConnection mbsc,
        ObjectName objectName,
        Class<T> intf,
        MBeanOptions options);
    ...
}

Using this, you can create a proxy like this:

SomethingMXBean proxy = JMX.newMXBeanProxy(
    mbsc, objectName, SomethingMXBean.class, options);

Here the options object can be the same as before.

This satisfies use case 3 above.

Interoperation when there are custom types

MXBeans map arbitrary Java types to Open Types. The addition of custom mappings doesn't change this - the result still has to be Open Types. So for generic clients like JConsole, nothing changes when custom types are added into the mix.

A client that is aware of the MXBean interfaces in use (like SomethingMXBean) can construct a proxy. To do that, it must have the interface available. If the interface has a @MXBeanMappingFactoryClass annotation, or if it contains a type that has a @MXBeanMappingClass annotation, then the classes referenced by those annotations must be present in the client too. It usually isn't any more difficult to arrange for the mapping classes to be present than to arrange for the original MXBean interface to be present.

If the mapping has been specified on the server using an explicit MXBeanMappingFactory, then the same or an equivalent factory must be used on the client. This is the case where there is the most risk of inconsistency. To help check that client and server are using the same MXBeanMappingFactory, the Descriptor of an MXBean using an MXBeanMappingFactory will contain a field naming the factory class, MyLinkedListMappingFactory in the examples above.

Some details

Here are some details I omitted above for clarity.

The methods toOpenValue and fromOpenValue have inconsistent throws clauses. This reflects a similar inconsistency in the MXBean spec, itself due to historical reasons.

The Java type and Open Type in MXBeanMapping must be supplied to the constructor by the subclass, and cannot be changed thereafter. This could be a limitation but I cannot currently see any cases where you would not be able to supply values to the super-constructor call. MXBeanMapping is a class and not an interface for a number of reasons, notably that it allows us to have final methods (getOpenType, getOpenClass, getJavaType) and methods with default implementations (checkReconstructible). MXBeanMappingFactory is a class not an interface so that we can add methods to it in later versions of the API if necessary.

The method MXBeanMapping.checkReconstructible() is used to determine if it is possible to map back from a value of the OpenType to a value of the original Java type. The default implementation does nothing. A subclass can override this method to throw OpenDataException if this mapping is not possible. See the MXBean specification for a discussion of reconstructible types.

MXBeanMappingClass can be defined like this:

@Documented @Retention(RUNTIME) @Target(TYPE) @Inherited
public @interface MXBeanMappingClass {
    Class<? extends MXBeanMapping> value();
}

The use of Class<? extends MXBeanMapping> means that if you write @MXBeanMappingClass(Something.class) and Something is not a subclass of MXBeanMapping, then you will get a compile error.

The class mentioned in the annotation must have a public constructor with a single parameter of type java.lang.reflect.Type. Unfortunately we can't get the compiler to check that!

The @Inherited annotation implies that if you subclass MyLinkedList then the subclass will inherit the same MXBeanMapping, which might not be what you want. But not inheriting the mapping seems more likely to surprise users.

The signature of MXBeanMappingFactory.mappingForType is this:

public MXBeanMapping mappingForType(Type t, MXBeanMappingFactory f)
    throws OpenDataException

The MXBeanMappingFactory parameter ensures that the same mappings are applied recursively. So if the type to be mapped is MyLinkedList[] in the example above, the mapping will proceed as follows:

MyLinkedListMappingFactory.mappingForType(MyLinkedList[].class, MyLinkedListMappingFactory) →
    MXBeanMappingFactory.DEFAULT.mappingForType(MyLinkedList[].class, MyLinkedListMappingFactory) →
        MyLinkedListMappingFactory.mappingForType(MyLinkedList.class, MyLinkedListMappingFactory)
        ← MyLinkedListMapping
    ← ArrayMapping(MyLinkedListMapping)
← ArrayMapping(MyLinkedListMapping)

where ArrayMapping is the private class that the default mapping factory uses to map arrays.

The key point here is that, even though MyLinkedListMappingFactory forwards any type it doesn't handle to the default mapping factory, the default factory will rerun any contained type through the user-specified mapping factory. So MyLinkedListMappingFactory doesn't have to recognize MyLinkedList[] or List<MyLinkedList> or whatever in addition to plain MyLinkedList. The general rules for SomeType[] or List<SomeType> in the default factory will apply, and then MyLinkedListMappingFactory will be applied to the contained SomeType, and do the right thing if that is MyLinkedList.

Open questions

This is still a draft proposal, and some questions remain.

As I mentioned above, the exact type of the MBeanOptions parameter to various methods is yet to be determined.

Should the @MXBeanMappingFactory option be inherited? For example, if you define

public interface SubSomethingMXBean extends SomethingMXBean {...}

should you inherit the @MXBeanMappingFactory annotation from SomethingMXBean? I think the answer must be yes, but what if SubSomethingMXBean inherits from more than one MXBean interface, and they have different @MXBeanMappingFactory annotations?

Should the @MXBeanMappingFactory option be applicable to packages? A reminder that you can annotate packages by creating a file called package-info.java in the package with contents like this:

@javax.management.openmbean.MXBeanMappingFactory(MyLinkedListMappingFactory.class)
package com.example.mbeans;

If @MXBeanMappingFactory can apply to packages and is also inherited from superinterfaces then we may have some complicated rules for precedence between the two.

Conclusion

In conclusion, we're specifying something quite simple: how to extend the standard MXBean mappings with custom mappings. But the details turn out not to be so simple!

Acknowledgements

This specification has been discussed in the JSR 255 Expert Group. Mandy Chung also had some very helpful comments.

[Tags: , .]

Making a JMX connection with a timeout

Posted by emcmanus on May 23, 2007 at 01:23 PM | Permalink | Comments (9)

One question I encounter frequently about the JMX Remote API is how to reduce the time taken to notice that a remote machine is dead when making a connection to it. The default timeout is typically a couple of minutes! Here's one way to do it.

Probably the cleanest technique for connection timeouts in general is to set a connection timeout on the socket. The idea is that instead of using...

Socket s = new Socket(host, port);

...you use...

SocketAddress addr = new InetSocketAddress(host, port);
Socket s = new Socket();
s.connect(addr, timeoutInMilliSeconds);

The problem is that this is at a rather low level. If you're making connections with the JMX Remote API you usually don't see Socket objects at all. It's still possible to use this technique, but it requires a certain amount of fiddling, and the particular fiddling you need depends on which connector protocol you are using.

A lot of the time, a much simpler and more general technique is applicable. You simply create the connection in another thread, and you wait for that thread to complete. If it doesn't complete before your timeout, you just abandon it. It might still take two minutes to notice that the remote machine is dead, but in the meantime you can continue doing other things.

If you're making a lot of connections to a lot of machines, you might want to think twice about abandoning threads, because you might end up with a lot of them. But in the more typical case where you're just making one connection, this technique may well be for you.

Assuming you're using at least Java SE 5, you'll certainly want to use java.util.concurrent to manage the thread creation and communication. There are a few ways of doing it, but the easiest is probably a single-thread executor.

The method below allows you to connect to a given JMXServiceURL with a timeout of five seconds like this:

JMXConnector jmxc = connectWithTimeout(jmxServiceURL, 5, TimeUnit.SECONDS);

My first cut at the problem

In my first version of this entry, I proposed a solution with the following outline.

JMXConnector connectWithTimeout(JMXServiceURL url, long timeout, TimeUnit unit) {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<JMXConnector> future = executor.submit(new Callable<JMXConnector>() {
	public JMXConnector call() {
	    return JMXConnectorFactory.connect(url);
	}
    });
    return future.get(timeout, unit);
}

Half an hour after posting, I suddenly realised that this version is incorrect. It reminds of the saying that for every complex problem there is a solution that is simple, obvious, and wrong.

This solution does the r