Java Dynamic Management Kit 5.1 Tutorial

Part IV Agent Services

The minimal and base agents presented in Part II are manageable but are created empty. In this part we will examine agent services, MBeans that interact with your resources to provide management intelligence at the agent level. These services allow your agents to perform local operations on their resources that used to be performed by the remote manager. This frees you from having to implement them in the management application, and it reduces the amount of communication throughout your management solution.

In the Java dynamic management architecture, agents perform their own monitoring, avoiding constant manager polling. Agents also handle their own logical relations between objects, providing advanced operations that no longer require a relational database in the manager.

Java dynamic management agents are also much smarter, using discovery to be aware of their peers. They also connect to other agents to mirror remote resources, thus providing a single point of entry into a whole hierarchy of agents. Finally, agents are freed from their initial environment settings through dynamic downloading, allowing managers to push new classes to an agent effectively. As new resources and new services are developed, they can be loaded into deployed agents, boosting their capabilities without affecting their availability.

The benefit of agent services is increased by their dynamic nature. Agent services can come and go as they are required, either as determined by the needs of an agent's smart resources or on demand from the management application. The agent capability of self-management through the services is completely scalable: small devices might only allow monitors with some logic, whereas a server might embed the algorithms and networking capabilities to oversee its terminals autonomously.

This part contains the following chapters:

Chapter 12 M-Let Class Loader

The “dynamic” in Java Dynamic Management Kit (Java DMK) not only stands for dynamic loading, it also stands for dynamic downloading. The agent service that provides this functionality is the m-let class loader. M-let stands for management applet, an HTML-style tag that tells the class loader how to retrieve the desired class. Using this information, the m-let loader can retrieve an MBean class from a remote location given as a URL and create it in the agent.

The m-let resides in a separate text file that acts as a loading manifest. The contents of the m-let file let you specify any number of classes to load, possibly a different source for each, arguments for the class constructor, and the object name for the instantiated MBean. Because this mechanism is sometimes too heavy, the m-let loader can also be used to load classes directly and create MBeans in the agent.

The m-let loader is a service implemented as an MBean, so it can be called either directly by the agent or remotely by a management application. It can also be managed remotely, which allows a manager to effectively “push” MBeans to an agent: the manager instantiates the m-let loader in an agent and instructs it to load classes from a predetermined location.

This chapter covers the following topics:

12.1 M-Let Loader

In the Java DMK 5.1, the m-let loader is a class loader object that extends the URLClassLoader class of the java.net package to simplify the downloading service it provides.

The m-let loader service is an instance of the MLet class in the javax.management.loading package. It is also an MBean that can be accessed remotely. It provides m-let file loading and a shortcut method. In addition, can be used directly as a class loader, without requiring an m-let file.

We will start by demonstrating the usage of the m-let service as it would be used in an agent or in an MBean. In our example, the agent application creates an MBean server and then the m-let loader.


Example 12–1 Instantiating the MLet Class

// Parse command line arguments.
[...]

// Instantiate the MBean server
MBeanServer server = MBeanServerFactory.createMBeanServer();
String domain = server.getDefaultDomain();

// Create a new MLet MBean and add it to the MBeanServer.
String mletClass = "javax.management.loading.MLet";
ObjectName mletName = new ObjectName(domain + ":name=" + mletClass);
server.createMBean(mletClass, mletName);

There is no special initialization that needs to be done before loading classes through an m-let file.

12.1.1 Loading MBeans from a URL

In order to download an MBean, we must first have its corresponding m-let definition in an HTML file. In our example, we define the following file with two MLET tags.


Example 12–2 The M-Let File

<HTML>
<MLET
  CODE=EquilateralTriangle.class
  ARCHIVE=EquilateralTriangle.jar
  NAME=MLetExample:name=EquilateralTriangle,id=1
>
<ARG TYPE=java.lang.Integer VALUE=8>
</MLET>
<MLET
  CODE=EquilateralTriangle.class
  ARCHIVE=EquilateralTriangle.jar
  NAME=MLetExample:name=EquilateralTriangle,id=2
>
<ARG TYPE=java.lang.Integer VALUE=15>
</MLET>
</HTML>

This file tells the m-let loader to create two MBeans with the given object names, using the given classes in the JAR files. The JAR files must be located in the same directory as this file, regardless of whether the directory is on a local or remote host. The MLET tag can also specify a CODEBASE, which is an alternate location for the JAR file. The MLET tag is fully defined in the JMX specification.

To download the MBeans specified in the m-let file we call the getMBeansFromURL method and analyze the result.


Example 12–3 Calling the getMBeansFromURL Method

// the url_2 string is read from the command line
echo("\tURL = " + url_2);
Object mletParams_2[] = {url_2};
String mletSignature_2[] = {"java.lang.String"};
Set mbeanSet = (Set) server.invoke(mletName, "getMBeansFromURL",
     mletParams_2, mletSignature_2);

for (Iterator i = mbeanSet.iterator(); i.hasNext(); ) {
    Object element = i.next();
    if (element instanceof ObjectInstance) {
        // Success, we display the new MBean's name
        echo("\tOBJECT NAME = 
			" + ((ObjectInstance)element).getObjectName());
    } else {
        // Failure, we display why
        echo("\tEXCEPTION = " + ((Throwable)element).getMessage());
    }
}

The m-let loader is the class loader, and it handles just a list of code-bases that it has accessed directly. You can view this list by calling the getURLs method of the m-let loader MBean.

This behavior means that the getMBeansFromURL method does not need to return the object names of class loaders it has used. Instead it just returns either the object instance of the downloaded and registered MBean or a Throwable object in case of an error or an exception. These are returned in a Set object containing as many elements as there are MLET tags in the target m-let file.

12.1.2 Shortcut for Loading MBeans

This behavior also simplifies any repeated loading of the classes after they have been loaded from an m-let file. Because the m-let loader has already used the code-base of the MBean, it is available to be used again. All you need to do is specify the object name of the m-let loader as the class loader when creating the MBean.

You can also load other MBeans in the same code-base, once the code-base has been accessed by a call to the getMBeansFromURL method. In our example we will just download another MBean of the EquilateralTriangle class.


Example 12–4 Reloading Classes in the M-Let Class Loader

// Create another EquilateralTriangle MBean from its class
// in the EquilateralTriangle.jar file.
String triangleClass = "EquilateralTriangle";
ObjectName triangleName = new ObjectName(
    "MLetExample:name=" + triangleClass + ",id=3");
Object triangleParams[] = {new Integer(20)};
String triangleSignature[] = {"java.lang.Integer"};

server.createMBean(triangleClass, triangleName, mletName,
                   triangleParams, triangleSignature);

Again, loading classes from known code-bases or reloading a class directly from its JAR file implies that the agent or MBean programmer has some knowledge of the code-bases and JAR file contents at runtime.

12.1.3 Loading MBeans Directly

Because the m-let loader object is a class loader, you can use it to load classes directly, without needing to define an m-let file.

Before you can load an MBean directly, you need to add the URL of its code-base to the m-let loader's internal list. Then we just use the m-let loader's object name as the class loader name when creating the MBean. Here is the code to do this in the agent example.


Example 12–5 Using the M-Let MBean as a Class Loader

// Add a new URL to the MLet class loader
// The url_1 string is read from the command line
Object mletParams_1[] = {url_1};
String mletSignature_1[] = {"java.lang.String"};
server.invoke(mletName, "addURL", mletParams_1, mletSignature_1);

// Create a Square MBean from its class in the Square.jar file.
String squareClass = "Square";
ObjectName squareName = new ObjectName(
    "MLetExample:name=" + squareClass);
Object squareParams[] = {new Integer(10)};
String squareSignature[] = {"java.lang.Integer"};
server.createMBean(squareClass, squareName, mletName,
                   squareParams, squareSignature);

You only need to add the URL to the m-let loader the first time you download a class. Once it is added, you can load it as many times as necessary by calling createMBean directly.

Because this loading mechanism does not use the MLET tag, the programmer must ensure that either the downloaded class provides its own object name or, as in Example 12–5, the agent provides one.

The fact that the m-let loader is also a class loader into which you can load multiple URLs raises the issue of name spaces. If there are two classes with the same name within the code-bases defined by the set of all URLs, the m-let loader will load one of them non-deterministically. To specify one of them precisely, do not add the URL of the second code-base to the m-let loader. Instead, create a second m-let loader MBean to which you can add the URL for the second version of the class. In this case, you will have one m-let MBean that can load one version of the class and another m-let MBean that can load the other.

12.1.4 Running the M-Let Agent Example

This example is located in the examplesDir/current/MLet/ directory. See “Directories and Classpath” in the Preface for details.

In our example, we have two MBeans representing geometrical shapes. Before running the example, we compile them and create a JAR file for each. We also compile the agent application at the same time.


$ cd examplesDir/current/MLet/
$ javac -classpath classpath *.java

$ jar cf Square.jar Square.class SquareMBean.class
$ rm Square.class SquareMBean.class

$ jar cf EquilateralTriangle.jar EquilateralTriangle.class \
EquilateralTriangleMBean.class
$ rm EquilateralTriangle.class EquilateralTriangleMBean.class

The agent command line requires you to specify first the URL of a JAR file for directly loading the Square class, then the URL of the m-let file. We have left these files in the examples directory, but you could place them on a remote host. With the Korn shell on the Solaris platform, type the following command:


$ java -classpath classpath Agent \ 
file:${PWD}/Square.jar file:${PWD}/GeometricShapes.html

In the output of the agent, you can see it create the m-let loader MBean, and then download classes to create MBeans. It starts with the direct loading of the Square class, and then loads from the HTML file that specifies two EquilateralTriangle MBeans to be loaded. Once these have been loaded, we can see the third one that is loaded through the class loader shortcut.

The agent then starts an HTML adaptor so that we can easily view the new MBeans. In them we can see that the values contained in the ARG tags of the m-let file were used to initialize the MBeans. Point your web browser to the following URL and click on the MBeans in the MLetExample domain:

http://localhost:8082/

When you have finished, press Control-C in the window where you started the agent.

12.2 Secure Class Loading

Because class loading exposes an agent to external classes, the Java DMK offers security within the m-let service.

12.2.1 Code Signing

Code signing is a security measure that you can use to identify the originator of a downloaded class. The m-let service will enforce code signatures if it is instantiated in secure mode. One of the constructors of the MLet class takes a boolean parameter that specifies the security mode. For obvious security reasons, the security mode cannot be modified once the m-let service is instantiated.

When the m-let service is running in secure mode, it will only load classes and native libraries that are signed by a trusted party. A trusted party is identified by a key: this key was used to sign the code and a copy of the key is given to all parties that want to download the signed class. Therefore, you must identify trusted keys in your agent before attempting to download their signed classes.


Note –

Downloading native libraries always requires a custom security manager, regardless of whether they are trusted or not.


In the MLet class, security is not determined when you instantiate the m-let service. Rather, security is enabled or disabled for your entire agent application, including any class loaders used by the m-let service.

To enable security, start your agent applications with the java.lang.SecurityManager property on the command line. Then, when the m-let service loads a class through one of its class loaders, the class loader will check the origin and signature of the class against the list of trusted origins and signatures.

The tools involved in signing a class file are the jar, keytool, and jarsigner utilities. On the host where the agent application will download a class, you define a set of permissions for signatures and URL origins. Then, you need to use the policytool utility to generate a java.policy file containing the trusted signatures. Refer to the Java 2 Platform Standard Edition (J2SE) documentation for the description of these utilities.

When the agent application is started with a security manager, it will check this policy file to ensure that the origin and signature of a downloaded class match a trusted origin and a trusted signature. If they do not match, the code is not trusted and cannot be loaded.

When the agent application is started without the security manager, all classes and native libraries can be downloaded and instantiated, regardless of their origin and signature, or lack thereof.

Chapter 13 Relation Service

The relation service defines and maintains logical relations between MBeans in an agent. It acts as a central repository of relation types and relation instances, and it ensures the consistency of all relations it contains. Local and remote applications that access the agent can define new types of relations and then declare a new instance of a relation between MBeans.

You can only create relations that respect a known relation type, and the service allows you to create new relation types dynamically. Then you can access the relations and retrieve the MBeans in specific roles. The relation service listens for MBeans being unregistered and removes relation instances that no longer fulfill their relation type. It sends notifications of its own to signal when relation events occur.

Java Dynamic Management Kit (Java DMK) also exposes the interface classes for defining your own relation objects. By creating relation instances as objects, you can implement them as MBeans that can expose operations on the relation they represent.

Like the other services, the relation service is instrumented as an MBean, allowing remote access and management.

The code samples in this topic are taken from the files in the Relation directory located in the main examplesDir (see “Directories and Classpath” in the Preface).

This chapter covers the following topics:

13.1 Defining Relations

A relation is composed of named roles, each of which defines the cardinality and class of MBeans that will be put into association with the other roles. A set of one or more roles defines a relation type. The relation type is a template for all relation instances that associate MBeans representing its roles. We use the term relation to mean a specific instance of a relation that associates existing MBeans according to the roles in its defining relation type.

For example, we can say that Books and Owner are roles. Books represents any number of owned books of a given MBean class, and Owner is a single book owner of another MBean class. We might define a relation type containing these two roles and call it Personal Library. It represents the concept of book ownership.

The following diagram represents this sample relation, as compared to the unified modeling language (UML) modeling of its corresponding association.

Figure 13–1 Comparison of the Relation Models

Diagram comparing the JMX and UML relation models

There is a slight difference between the two models. The UML association implies that each one of the Books can only have one owner. Our relation type only models a set of roles, guaranteeing that a relation instance has one Owner MBean and any number of MBeans in the role of Books.


Note –

The relation service does not do inter-relation consistency checks; if they are needed they are the responsibility of the designer. In our example, the designer needs to ensure that the same book MBean does not participate in two different Personal Library relations, while allowing it for an owner MBean.


In the rest of this topic, we will see how to handle the roles, relation types and relation instances through the relation service. We will use the names and relations presented in the programming example. First of all, we instantiate the relation service as we would any other MBean and register it in our agent's MBean server.


Example 13–1 Relation Service MBean

// Build ObjectName of RelationService
	//        
String relServClassName = RelationService.class.getName();
ObjectName relServObjName = createMBeanName(relServClassName, 1);

[...]

Object[] params = new Object[1];
params[0] = new Boolean(true);
String[] signature = new String[1];
signature[0] = "boolean";

server.createMBean(theRelServClassName, theRelServObjName,
                   params, signature);

The relation service exposes an attribute called Active that is false until its MBean is registered with the MBean server. All of the operations that handle relation or role MBeans, either directly or indirectly, will throw an exception when the service is not active.


13.1.1 Defining Role Information

Before we can create relation instances, we need a relation type, and before we can define a relation type, we need to represent the information about its roles. This information includes:

The name can be any string that is manipulated by the Java String class. It will be used to retrieve the corresponding MBeans when accessing a relation instance. The multiplicity is limited to the range between the minimum and maximum number of MBeans required for this role to fulfilled. The read-write permissions apply to all of the MBeans in the role, since the value of a role is read or written as a complete list.

The role information is represented by the RoleInfo class. In our example, we define two different roles.


Example 13–2 Instantiating RoleInfo Objects

// Define two roles in an array
// - container: SimpleStandard class/read-write access/multiplicity: 1..1
// - contained: SimpleStandard class/read-write access/multiplicity: 0..n

RoleInfo[] roleInfoArray = new RoleInfo[2];
String role1Name = "container";
roleInfoArray[0] =
    createRoleInfo( role1Name, "SimpleStandard",
                  true, true,
                  1, 1,
                  null);

String role2Name = "contained";
roleInfoArray[1] =
    createRoleInfo( role2Name, "SimpleStandard",
                  true, true,
                  0, -1,
                  null);

We build an array of RoleInfo objects that is intended to define a relation type, so it needs to define a valid set of roles. All role names must be unique within the array, and none of the array's elements can be null. Also, the minimum and maximum cardinalities must define a range of at least one integer.

13.1.2 Defining Relation Types

We define a relation type in the relation service by associating a name for the relation type with a non-empty array of RoleInfo objects. These are the parameters to the service's createRelationType method that we call through the MBean server.


Example 13–3 Defining a Relation Type

try {
    String relTypeName = "myRelationType";

    Object[] params = new Object[2];
    params[0] = theRelTypeName;
    params[1] = theRoleInfoArray;
    String[] signature = new String[2];
    signature[0] = "java.lang.String";
    // get the string representing the "RoleInfo[]" object
    try {
         signature[1] =
                   (theRoleInfoArray.getClass()).getName();
         } catch (Exception exc) {
             throw exc;
         }
         server.invoke(theRelServObjName,
                         "createRelationType",
                         params,
                         signature);
    } catch (Exception e) {
         echo("\tCould not create the relation type " 
              + relTypeName);
         printException(e);
    }
}

The relation type name given by the calling process must be unique among all relation type names already created in the relation service. Relations will refer to this name to define their type, and the service will verify that the roles of the relation match the role information in this type.

The relation service provides methods for managing the list of relation types it stores. The removeRelationType removes the type's definition from the relation service and also removes all relation instances of this type. This mechanism is further covered in 13.2.3 Maintaining Consistency.

Other methods give the list of all currently defined relation types or the role information associated with a given type. Role information is always obtained through the name of the relation type where it is defined. Here we show the subroutine that our example uses to print out all role and type information.


Example 13–4 Retrieving Relation Types and Role Information

try {
    echo("\n-> Retrieve all relation types");
    Object[] params1 = new Object[0];
    String[] signature1 = new String[0];
    ArrayList relTypeNameList = (ArrayList)
    (server.invoke( relServObjName, "getAllRelationTypeNames",
                    params1, signature1));

    for (Iterator relTypeNameIter = relTypeNameList.iterator();
         relTypeNameIter.hasNext(); ) {
        String currRelTypeName = (String)(relTypeNameIter.next());
        echo("\n-> Print role info for relation type " +
             currRelTypeName);
        Object[] params2 = new Object[1];
        params2[0] = currRelTypeName;
        String[] signature2 = new String[1];
        signature2[0] = "java.lang.String";
        ArrayList roleInfoList = (ArrayList)
            (server.invoke( relServObjName,"getRoleInfos",
                            params2, signature2));
        printList(roleInfoList);
    }
} catch (Exception e) {
    echo("\tCould not browse the relation types");
    printException(e);
}

13.1.3 Creating Relations

Now that we have defined a relation type, we can use it as a template for creating a relation. A relation is a set of roles that fulfills all of the role information of the relation type. The Role object contains a role name and value which is the list of MBeans that fulfills the role. The RoleList object contains the set of roles used when setting a relation or getting its role values.

In order to create a relation we must provide a set of roles whose values will initialize the relation correctly. In our example we use an existing SimpleStandard MBean in each role that we have defined. Their object names are added to the value list for each role. Then each Role object is added to the role list.


Example 13–5 Initializing Role Objects and Creating a Relation

[...] // define object names and create SimpleStandard MBeans

// Instantiate the roles using the object names of the MBeans
ArrayList role1Value = new ArrayList();
role1Value.add(mbeanObjectName1);
Role role1 = new Role(role1Name, role1Value);

ArrayList role2Value = new ArrayList();
role2Value.add(mbeanObjectName2);
Role role2 = new Role(role2Name, role2Value);

RoleList roleList1 = new RoleList();
roleList1.add(role1);
roleList1.add(role2);

String relId1 = relTypeName + "_internal_1";

try {
    Object[] params = new Object[3];
    params[0] = theRelId1;
    params[1] = theRelTypeName;
    params[2] = roleList1;
    String[] signature = new String[3];
    signature[0] = "java.lang.String";
    signature[1] = "java.lang.String";
    signature[2] = "javax.management.relation.RoleList";
    server.invoke(theRelServObjName, "createRelation",
                  params, signature);
} catch(Exception e) {
    echo("\tCould not create the relation " + RelId1);
    printException(e);
}

The createRelation method will raise an exception if the provided roles do not fulfill the specified relation type. You can omit Role objects for roles that allow a cardinality of 0; their values will be initialized with an empty list of object names. The relation service will check all provided object names to ensure they are registered with the MBean server. Also, the relation identifier name is a string that must be unique among all relations in the service.

The corresponding removeRelation method is also exposed for management operations. It is also called internally to keep all relations coherent, as described in 13.2.3 Maintaining Consistency. In both cases, removing a relation means that you can no longer access it through the relation service, and the isRelation operation will return false when given its relation identifier. Also, its participating MBeans will no longer be associated through the roles in which they belonged. The MBeans continue to exist unaltered otherwise and can continue to participate in other relations.

13.2 Operations of the Relation Service

In addition to the creation and removal methods for relation types and instances, the relation service provides operations for finding related MBeans, determining the role of a given MBean, and accessing the MBeans of a given role or relation.

13.2.1 Query Operations

Once relations have been defined, the relation service allows you to do searches based on the association of objects that the relations represent. The following operations perform queries on the relations:

13.2.2 Accessing Roles

Once you have a relation identifier, you will probably want to access its role values. The relation service provides getters and setters for roles, as well as bulk operations to get or set several or all roles in a relation. Remember that the value of a role is a list of MBeans, and a role is identified by a name string.

Input parameters to setter operations are the same Role and RoleList classes that are used to create relations. Bulk getters and setters return a RoleResult object that contains separate lists for successful and failed individual role operations.

Inside a role result, the list of roles and values that were successfully accessed are given in a RoleList instance. The information about roles that could not be read or written is returned in a RoleUnresolvedList. This list contains RoleUnresolved objects that name the role that could not be accessed and an error code explaining the reason, such as an invalid role name or the wrong cardinality for a role. The error codes are defined as static final fields in the RoleStatus class.

13.2.3 Maintaining Consistency

All relation service operations that set the role of a relation always verify that the role value fulfills its definition in the relation type. An incorrect MBean type, the wrong cardinality of the role, or an unknown role name will prevent that role from being modified, guaranteeing that the relation always fulfills it relation type.

As we shall see in 13.3 Objects Representing Relations, relations and relation types can also be objects that are external to the relation service. In this case, they are responsible for maintaining their own role-consistency. The relation service MBean exposes methods to assist these objects in verifying that their roles conform to their defined relation type. In all cases of an external implementation of a relation, the object designer is responsible for ensuring its coherence.

Removing a relation type can cause existing relations to cease to have a defining type. The policy of the removeRelationType operation is to assume that the caller is aware of existing relations of the given type. Instead of forbidding the operation, this method removes the relations that were defined by the given type. It is the designer's responsibility to first call the findRelationsOfType operation to determine if any existing relations will be affected.

MBeans participating in a relation can be removed from the MBean server by some other management operation, thereby modifying the cardinality of a role where they were referenced. In order to maintain consistency in this case, the relation service must remove all relations in which a role no longer has the cardinality defined in its relation type. The process of determining invalid relations and removing them is called purging.

The relation service listens for unregistration notifications of the MBean server delegate, and will need to purge its MBeans whenever one is received. It must determine if the MBean removed was involved in any relations, and if so, whether its removal violates the cardinality of each role where it appears. The relation service exposes the boolean PurgeFlag attribute that the programmer must set to determine whether purges are done automatically or not.

When the purge flag is true, the relation service will purge its data immediately after every unregistration notification. However, the purge operation can be resource intensive for large sets of relation data. In this case the managing application can set the purge flag to false and only call the purgeRelations operation to purge the data when needed.

For example, if there are many MBean unregistrations and few relation operations, it might make sense to only purge the relation service manually before each operation. Or the automatic purge flag can be temporarily set to false while executing time-critical operations that need to remove MBeans, but that will not access the relation service.

There are two possible consequences of an unpurged relation service. Roles in a relation can reference object names that no longer have an associated MBean. Or worse, the object name might have been reassigned to another MBean, leading to a totally incoherent state. The designer of the management solution is responsible for setting the purge policy so that operations will always access consistent relation values.

13.2.4 Relation Service Notifications

The relation service is a notification broadcaster that notifies listeners of events affecting relations. It sends RelationNotification objects in the following cases:

There are three equivalent notification types for events affecting relations defined as external MBeans (see 13.3 Objects Representing Relations). In addition to the role and relation information, these notifications contain the object name of this MBean.

13.3 Objects Representing Relations

When creating relation types and instances, their representations are handled internally and only accessed through the interface of the relation service. However, the service also allows you to create external objects that represent relations, and then add them under the service's control to access them through its operations.

One advantage of representing relation types and instances as classes is that they can perform their initialization in the class constructor. Applications can then instantiate the classes and add them to the relation service directly, without needing to code for their creation. The relation type classes can be downloaded to agent applications for use throughout the management architecture.

Another advantage is that relations are represented as MBeans and must be registered in the MBean server. This means that management applications can get role information directly from the relation MBean instead of going through the relation service.

The main advantage of an external relation class is that it can be extended to add properties and methods to a relation. They can be accessible through attributes and operations in the relation MBean so that they are also exposed for management. These extensions can represent more complex relations and allow more flexible management architectures.

With the power of manipulating relations comes the responsibility of maintaining the relation model. A relation MBean can expose an operation for adding an object name to a role, but its implementation must first ensure that the new role value will conform to the relation's type. Then, it must also instruct the relation service to send a role update notification. The relation service MBean exposes methods for maintaining consistency and performing required operations. It is the programmer's responsibility to call the relation service when necessary in order to maintain the consistency of its relation data.

13.3.1 RelationTypeSupport Class

A class must implement the RelationType interface in order to be considered a representation of a relation type. The methods of this interface are used to access the role information that makes up a relation type. Since relation types are immutable within the relation service, there are no methods for modifying the role information.

The RelationTypeSupport class is the implementation of this interface that is used internally by the relation service to represent a relation type. By extending this class, you can quickly write new relation type classes with all the required functionality. The class has a method for adding roles to the information that is exposed; this method can be called by the class constructor to initialize all roles. Our simple example does just this, and there is little other functionality that can be added to a relation type object.


Example 13–6 Extending the RelationTypeSupport Class

import javax.management.relation.*;

public class SimpleRelationType extends RelationTypeSupport {

    // Constructor
    public SimpleRelationType(String theRelTypeName) {

        super(theRelTypeName);

        // Defines the information for two roles
        // - primary: SimpleStandard class/read-write/cardinality=2
        // - secondary: SimpleStandard class/read-only/cardinality=2
        try {
            RoleInfo primaryRoleInfo =
                new RoleInfo("primary", "SimpleStandard",
                             true, true,
                             2, 2,
                             "Primary description");
            addRoleInfo(primaryRoleInfo);

            RoleInfo secondaryRoleInfo =
                new RoleInfo("secondary", "SimpleStandard",
                             true, false,
                             2, 2,
                             "Secondary description");
            addRoleInfo(secondaryRoleInfo);
        } catch (Exception exc) {
            throw new RuntimeException(exc.getMessage());
        }
    }
}

We now use our class to instantiate an object representing a relation type. We then call the relation service's addRelationType operation to make this type available in the relation service. Thereafter, it is manipulated through the service's operations and there is no way to distinguish it from other relation types that have been defined.


Example 13–7 Adding an Externally Defined Relation Type

String usrRelTypeName = "SimpleRelationType";
SimpleRelationType usrRelType =
    new SimpleRelationType("SimpleRelationType");
try {
    Object[] params = new Object[1];
    params[0] = usrRelType;
    String[] signature = new String[1];
    signature[0] = "javax.management.relation.RelationType";
    server.invoke( relServObjName, "addRelationType",
                   params, signature);
} catch(Exception e) {
    echo("\tCannot add user relation type");
    printException(e);
}

The role information defined by a relation type should never change once the type has been added to the relation service. This is why the RelationTypeSupport class is not an MBean: it would make no sense to manage it remotely. All of the information about its roles is available remotely through the relation service MBean.

13.3.2 RelationSupport Class

The external class representation of a relation instance must implement the Relation interface so that it can be handled by the relation service. The RelationSupport class is the implementation provided which is also used internally by the service.

The methods of the relation interface expose all of the information needed to operate on a relation instance: the getters and setters for roles and the defining relation type. Because the relation support class must represent any possible relation type, it has a constructor that takes a relation type and a role list, and it uses a generic mechanism internally to represent any roles.

You could implement a simpler relation class that implements a specific relation type, in which case it would know and initialize its own roles. The class could also interact with the relation service to create its specific relation type before adding itself as a relation.

However, the simplest way to define a relation object is to extend the RelationSupport class and provide any additional functionality you require. In doing so, you can rely on the relation support class's own methods for getting and setting roles, thereby taking advantage of their built-in consistency mechanisms.


Example 13–8 Extending the RelationSupport Class

import javax.management.ObjectName;
import javax.management.relation.*;

public class SimpleRelation extends RelationSupport
    implements SimpleRelationMBean {

    // Constructor
    public SimpleRelation(String theRelId,
                          ObjectName theRelServiceName,
                          String theRelTypeName,
                          RoleList theRoleList)
        throws InvalidRoleValueException,
               IllegalArgumentException {

        super(theRelId, theRelServiceName, theRelTypeName, theRoleList);
    }

    [...] // Implementation of the SimpleRelationMBean interface
}

Our MBean's SimpleRelationMBean interface extends the RelationSupportMBean in order to expose its operations for management. In order to represent a relation, the class must be an MBean registered in the MBean server. This allows the relation service to rely on unregistration notifications in order to find out if the object name is no longer valid.

When instantiating our SimpleRelation MBean, we use the relation type defined in Example 13–3. We also reuse the role list from Example 13–5 that contains a single MBean in each of two roles. Before adding the relation to the relation service, we must create it as an MBean in the MBean server. We then call the addRelation operation of the relation service with our relation's object name.


Example 13–9 Creating an External Relation MBean

// Using relTypeName="myRelationType"
// and roleList1={container,contained}

String relMBeanClassName = "SimpleRelation";
String relId2 = relTypeName + "_relMBean_2";
ObjectName relMBeanObjName1 = createMBeanName(relMBeanClassName, 2);
createRelationMBean(relMBeanObjName1,
                    relMBeanClassName,
                    relId2,
                    relTypeName,
                    roleList1,
                    relServObjName);
try {
    Object[] params1 = new Object[4];
    params1[0] = theRelId2;
    params1[1] = theRelServObjName;
    params1[2] = theRelTypeName;
    params1[3] = roleList1;
    String[] signature1 = new String[4];
    signature1[0] = "java.lang.String";
    signature1[1] = "javax.management.ObjectName";
    signature1[2] = "java.lang.String";
    signature1[3] = "javax.management.relation.RoleList";
    server.createMBean(theRelMBeanClassName, theRelMBeanObjName,
                       params1, signature1);

    addRelation(theRelMBeanObjName, theRelServObjName);

} catch(Exception e) {
    echo("\t Could not create relation MBean for relation " + relId2);
    printException(e);
}

// Add our new MBean as a relation to the relation service
try {
    Object[] params2 = new Object[1];
    params2[0] = theRelMBeanObjName;
    String[] signature2 = new String[1];
    signature2[0] = "javax.management.ObjectName";
    server.invoke(theRelServObjName, "addRelation",
                  params2, signature2);
} catch(Exception e) {
    echo("\t Could not add relation MBean " + relMBeanObjName);
    printException(e);
}

After a relation instance is added to the relation service, it can be accessed normally like other relations. Management operations can either operate on the MBean directly or access it through its identifier in the relation service. Two methods of the service are specific to external relation MBeans:

In our example, our MBean does not expose any functionality that interacts with the relation service. It relies fully on the support class and only adds the implementation of its own simple MBean interface. In a more realistic example, the MBean would expose attributes or operations related to its role values.

For example, Personal Library could be a unary relation type containing the Books role. We could then design an Owner MBean to be a relation of this type. In addition to exposing attributes about the owner, the MBean would give the number of books in the library, return an alphabetized list of book titles, and provide an operation for selling a book. All of these would need to access the role list, and the sale operation would need to modify the role to remove a book MBean.

All of these operations would need to keep the consistency of the relation. To do this, the relation service exposes several methods that relation MBeans must call:

13.4 Running the Relation Service Example

The examplesDir/current/Relation directory contains all of the files for the agent application and the associated MBeans.

To Run the Relation Service Example
  1. Compile all files in this directory with the javac command. For example, on the Solaris platform with the Korn shell, type:


    $ cd examplesDir/current/Relation/
    $ javac -classpath classpath *.java
    
  2. Start the relation service application with the following command. Be sure that the classes for the SimpleRelationType and SimpleRelation MBean can be found in its classpath.


    $ java -classpath classpath RelationAgent
    

    When started, the application first creates the RelationService MBean and then begins its long demonstration of operations on roles, types and relations. The output of each step is separated by a horizontal line to detect its starting point in the scrolling display.

  3. Press Enter to step through the example when the application pauses, or press Control-C at any time to exit.

Chapter 14 Cascading Service

The cascading service enables you to access the MBeans of a subagent directly through the MBean server of a master agent. The cascading service has been completely overhauled in Java Dynamic Management Kit (Java DMK) 5.1 to allow it to operate over the connector protocols defined by the Java Management Extensions (JMX) Remote API. The legacy cascading service is now deprecated. The examples of the legacy cascading service have been retained in Chapter 25, Legacy Cascading Agents, for reasons of backwards compatibility. However, when using legacy Java DMK connectors, you should use the new CasdingServiceMBean with wrapped legacy connectors rather than relying on the deprecated legacy cascading agent API.

The service is implemented in a CascadingServiceMBean, which makes it possible to mount source MBean servers (that are possibly located in subagents) into a target MBean server (that is located in a Master Agent) in a manner that is somewhat analogous to a File System mount operation. Several source MBean servers can be mounted in the same target MBean server, provided that different target paths are used. A source MBean server mounted from a subagent can itself be a Master Agent in which source MBean servers from another level of subagents are mounted, and so on and so on, forming a hierarchy of cascading agents.

By connecting to the root of an agent hierarchy, managers can have a single access point to many resources and services. All MBeans in the hierarchy are manageable through the top master agent, and a manager does not need to worry about their physical location. Like the other services, the cascading service is implemented as an MBean that can be managed dynamically. This enables the manager to control the structure of the agent hierarchy, adding and removing subagents as necessary.

In particular, the cascading service MBean can work with any protocol connector, including any custom implementation. Those supplied by the Java DMK give you the choice of using remote method invocation (RMI), or the JMX messaging protocol (JMXMP). The legacy connector protocols implemented by previous versions of Java DMK can also be used, as long as they have been wrapped for use with the JMX Remote API, as described in 9.5 Wrapping Legacy Connectors.

The cascading service also lets you specify a filter for selecting precisely the source MBeans that are cascaded in the target MBean server. This mechanism lets you limit the number of MBeans that are cascaded in the top agent of a large cascading hierarchy. For a general overview of the cascading service, see the Java Dynamic Management Kit 5.1 Getting Started Guide.

The code samples in this topic are from the files in the current/Cascading directory located in the main examplesDir (see Directories and Classpath in the preface).

This chapter contains the following topics:

14.1 CascadingService MBean

You should create a maximum of one CascadingService MBean in any target MBean server. The same CascadingService MBean can be used to mount multiple source MBean servers, that are located in multiple subagents in the Master Agent's target MBean server. The CascadingService creates one ProxyCascadingAgent behind the scenes for every mount operation it performs. The creation of that ProxyCascadingAgent is entirely hidden from the user. The complexity of the ProxyCascadingAgents is thus hidden by the CascadingService MBean. You should not attempt to use CascadingAgents directly, as was the case in earlier versions of Java DMK. Applications should always use the CascadingServiceMBean instead. The CascadingServiceMBean handles connections to the subagents and cascades all of that agent's registered MBeans into the master agent's target MBean server. No other classes are required or need to be generated to represent the MBeans.

The agent whose MBean server contains an active cascading service is called a master agent in relation to the subagent that is cascaded. The MBean server in which the MBeans are cascaded is called the target MBean server. An agent to which the cascading service is connected is called a subagent in relation to its master agent. Its MBean server is named the source MBean server. The source MBean server is the MBean server in which the MBeans actually reside. The CascadingService MBean creates target MBeans in the target MBean server to represent the subagent's source MBeans. For each source MBean, a CascadingProxy is registered behind the scenes in the target MBean server. See 14.2 Cascaded MBeans in the Master Agent for a description of these objects. The operation that makes it possible to cascade a source MBean server located in a subagent to the target MBean server in the Master Agent is called a mount operation, because of its similarity to a file system mount operation.

A master agent can have any number of subagents, each controlled individually by the same CascadingService MBean. Each mount operation is in fact implemented behind the scenes through a different CascadingAgent object created by the CascadingService MBean. The complexity of the CascadingAgent API is however completely abstracted by the CascadingServiceMBean. A subagent can itself contain cascading agents and cascaded MBeans, mounted from another layer of subagents, all of which will appear to be cascaded again in the master agent. This effectively enables cascading hierarchies of arbitrary depth and width.

Two master agents can also connect to the same subagent. This is similar to the situation where two managers connect to the same agent and can access the same MBean. If the implementation of a management solution permits such a condition, it is the designer's responsibility to handle any synchronization issues in the MBean.

The connection between two agents resembles the connection between a manager and an agent. The cascading service MBean relies on a connector client, and the subagent must have the corresponding connector server. The subagent's connector server must already be instantiated, be registered with its MBean server, and be ready to receive connections. The CascadingServiceMBean mount operation accepts a JMXServiceURL and a Map from which it obtains a JMXConnector from the JMXConnectorFactory. The mount operation will create the connection, and the unmount operation will close it.


Example 14–1 Mounting MBeans from a Subagent

[...]

private void mountSubagents(JMXServiceURL[] agentURLs) {
		 mountPoints = new String[agentURLs.length];
	    for (int i=0;i<agentURLs.length;i++) {
	         try {
               final String mountPointID =
		               cascadingService.mount(agentURLs[i],null,
                                         new ObjectName("ExportDomain:*"),
                                         "subagents/agent"+(i+1));

				     mountPoints[i] = mountPointID;
     		     echo(mountPointID);
	          } catch (Exception x) {
		           echo("\t!!! Could not mount subagent#"+(i+1)+" at " + 
		                agentURLs[i] + "\n\tError is: " + x);
		           x.printStackTrace();
		           echo("\nEXITING...\n");
		           System.exit(1);
	          }
	    }
}

	[...]

In Example 14–1, source MBean servers located in subagents are mounted using a defined JMXServiceURL, to cascade remote MBeans from the domain ExportedDomain. By default, all MBeans of the subagent are cascaded in the master agent, but in this example we provide an object name pattern so as only to select those in ExportedDomain. Example 14–1 is taken from the example class MasterAgent, in the directory examplesDir/current/Cascading. To emulate file system mount operations, a different target path for each mounted subagent is used. In this example, we use subagents/agent#x, where #x starts at 1, x is incremented for each subagent, and preserves the order of the JMX Service URLs that are given as input.


Note –

You should use a different target path for every mount operation. The Java DMK cascading service implementation does not enforce this rule, but applications that are concerned with naming coherency should not break it.



Example 14–2 Creating the Cascading Service

[...]

    private void createCascadingService() {

        printline();
        echo("Creating the CascadingService" +
             " MBean within the MBeanServer:");
        try {
	            CascadingService service  = new CascadingService();
             ObjectInstance   cascadingInstance =
                  server.registerMBean(service, null);
             echo("\tCLASS NAME  = " + cascadingInstance.getClassName());
             echo("\tOBJECT NAME = " + cascadingInstance.getObjectName());
	             cascadingService = service;
          } catch (Exception e) {
             echo("\t!!! Could not create the " +
                  CascadingService.class.getName() + " MBean !!!");
             e.printStackTrace();
             echo("\nEXITING...\n");
             System.exit(1);
          }
    }

[...]

As shown in Example 14–2, before the subagent's source MBean servers can be mounted, the CascadingService MBean must be created and registered in, or tied to, the master agent's target MBean server. The CascadingService MBean in this example is registered in the master agent's target MBean server with a null object name, so that it is registered with its default ObjectName, that is defined in the CascadingServiceMBean interface.


Example 14–3 Unmounting MBeans from a Subagent

[...]

private void unmountAll() {
      printline();

	      echo("Unregistering CascadingServiceMBean");
       try {
	          server.unregisterMBean(CascadingServiceMBean.
				                        CASCADING_SERVICE_DEFAULT_NAME);
	      } catch (Exception x) {
	          echo("\t!!! Could not unregister " + CascadingServiceMBean.
		            CASCADING_SERVICE_DEFAULT_NAME + "\n\tError is: " + x);
	          x.printStackTrace();
	          echo("\nEXITING...\n");
	          System.exit(1);
       } 

	       echo("Unmounting all mount points");

       final String mounts[] = cascadingService.getMountPointIDs();
	       for (int i=0;i<mounts.length;i++) {
	            try {
		              if (cascadingService.unmount(mounts[i]))
		                  echo("unmounted "+mounts[i]);
	            } catch (Exception x) {
		              echo("\t!!! Could not unmount " + mounts[i] +
		                   "\n\tError is: " + x);
		              x.printStackTrace();
		              echo("\nEXITING...\n");
		              System.exit(1);
	            }
	       }
  }

[...]

When the master agent is stopped, it stops the cascading service by unmounting all the subagents that are currently mounted. This is done by first unregistering the CascadingServiceMBean from its target MBean server, to ensure that nobody can create new mount points while the removal is ongoing. Then, for each currently mounted mount point, the unmount operation is called, so that all proxies for the associated cascaded MBeans are unregistered from the target MBean server. The MBean server delegate in the master agent sends an unregistration notification for each cascaded MBean as it is removed.

14.2 Cascaded MBeans in the Master Agent

Once the the cascading service has mounted a source MBean server in the target MBean server, you can interact directly through the target MBean server with the target MBeans representing the subagent's MBeans. You can access and manage these MBeans as if you were connected to the subagent and accessing or managing the original MBeans. The target MBeans that you interact with are in fact CascadingProxy MBean objects registered in the master agent's target MBean server with the same object name as the source MBean, but with a domain that is prefixed by the target path used in the mount operation.

All management operations that you can perform on the original MBean can be performed identically through the target MBeanServer, using that target's object name. You can modify attributes, invoke operations and add or remove listeners, all with exactly the same result as if the manager were connected to the subagent when performing the action.

The behavior of a target CascadingProxy MBean is to transmit the action transparently to the subagent's MBean and return with an answer or result. The actual computation is performed by the original MBean running in its own source MBean server.

In Example 14–4, there is a timer MBean that was created in the subagent. Once the subagent has been mounted in the target MBean server, the timer is operated through its local target CascadingProxy MBean. It is not possible to have the direct reference to a source MBean, but it is possible to obtain a local proxy using the MBeanServerInvocationHandler class, as if it were a local MBean registered in the target MBean server. The fact that the operations are actually routed to a subagent through a JMXConnector remains hidden.


Example 14–4 Managing Cascaded MBeans

    private void doCascadingOperations() {

        echo("\nPress Enter to continue...\n");
        waitForEnterPressed();
        printline();
        try {
             if (! cascadingService.isMounted(mountPoints[0])) {
		            echo(mountPoints[0] + ": \n\t" + "not mounted! ");
		            echo("Cannot do cascading operations.");
		            return;
	            }
            
             ObjectName timerName =
                new ObjectName("subagents/agent1/ExportDomain:type=Timer");
	             echo(">>> Get Timer MBean \""+timerName+"\"");
             TimerMBean timer = (TimerMBean) MBeanServerInvocationHandler.
               newProxyInstance(server, timerName, TimerMBean.class, true);

            echo("\n>>> Ask the Timer MBean to send a " +
                 "Timer Notification every 3 seconds");
            Date currentDate = new Date();
                  
            timer.addNotification("Timer","Message",null,
                                  new Date(currentDate.getTime() + 
                                  2),3000);

            timer.start();

            echo("\n>>> Add listener to the Timer MBean");
            server.addNotificationListener(timerName, this, null, null);
            echo("\tListener added successfully");
            echo("\nPress Enter to remove the listener from the " +
		             "Timer MBean...");
            waitForEnterPressed();

	    	      if (cascadingService.isMounted(mountPoints[0])) {
		             try {
                    server.removeNotificationListener(timerName, 
                    this);
		             } catch (Exception x) {
                    echo("Unexpected exception while unregistering 
                         listener:");
		                 echo("\tError is: " + x);
		             }
	            } else {
                 echo(mountPoints[0] + ": \n\t" + "already 
                      unmounted! ");
	            }

	        } catch (Exception e) {

            e.printStackTrace();
            System.exit(1);
        }
    }

Example 14–4 obtains a TimerMBean proxy for the Timer MBean from the subagent #1 ExportedDomain, and then performs operations on it. In particular, it registers for timer notifications and starts the timer.

To the managing application, the target MBean in the master agent target MBean server is the MBean. Unregistering a cascaded MBean in the master agent will not unregister the cascaded MBean in the subagent, but will only unregister the CascadingProxy from the target MBean server. You should not attempt to unregister MBeans mounted from a source MBean server through the target MBean server. If you want to create or unregister MBeans in a subagent, you should not do so through the CascadingService, but should use a JMXConnector directly connected to the subagent.

The cascading is almost totally transparent. A manager has no direct way of knowing whether an object is a cascaded object or not. It can attempt to guess the topology of the cascading hierarchy by examining the domain path of the object names, provided that the subagents have been mounted using a meaningful and coherent target path, but the underlying connectivity is completely transparent. The managing application can however be informed of failed or closed mount points by registering for notifications with the CascadingServiceMBean.

Likewise, some operations might fail due to the fact that the CascadingProxy object registered in the target MBean server is not the real source MBean. For example, unregistering the CascadingProxy will not cause the original source MBean to be unregistered. The CascadingProxy might reappear later if an external event makes the CascadingService update its view of the subagents. Similarly, trying to call addNotificationListener(ObjectName,ObjectName,...) with the object name of a target CascadingProxy as listener (namely the second parameter) will fail, because this operation cannot be routed to the source MBean.

14.2.1 Class of a Cascaded MBean

Proxy MBeans registered in the target MBeanServers when mounting source MBean servers are implemented as instances of the CascadingProxy class. The CascadingProxy MBean is locally registered in the target MBean server, and has the same MBeanInfo as its source MBean. In particular, if the source MBean is a model MBean, the MBeanInfo exposed by its CascadingProxy is ModelMBeanInfo. The exposed MBean information also contains the class name of the original MBean, and not the class name of the CascadingProxy. Exposing this borrowed MBeanInfo guarantees that the cascading service is as transparent as possible.

The symmetry of the Java dynamic management architecture means that this cascading mechanism is scalable to any number of levels. The cascaded object of a cascaded object is again an instance of the CascadingProxy class, and it borrows the same MBeanInfo. Any operation on the top target object is propagated to its source subagent, where the intermediate cascaded object will send it to its own source subagent, and so forth. The cost of cascading is the cost of accessing the subagents. The depth of your cascading hierarchy should be adapted to your management solution.

Because the cascading service MBean instantiates and controls all cascaded MBeans, the CascadingProxy class should never be instantiated through a management operation, nor by the code of the agent application. It is described here only to provide a better understanding of the internal behavior of the cascading service.

14.2.2 Cascading Issues

In this section, we explain some of the design issues that are determined by the implementation of the cascading service.

14.2.2.1 Dynamic Cascading

When an MBean is unregistered from the subagent, the cascading service automatically removes its corresponding target CascadingProxy MBean from the master agent's target MBean server. When a new MBean is registered in the subagent's source MBean server, the cascading service registers a new CascadingProxy mirroring this source MBean with the master agent's MBean server. Note that if an ObjectName pattern was provided when performing the mount operation, only those source MBeans whose source ObjectName satisfy that ObjectName pattern are considered.

Both these mechanisms scale to cascading hierarchies. Adding or removing an MBean in the subagent will trigger a notification that any cascading service connected to the subagent will receive. This will start a chain reaction up to the top of the hierarchy. Removing an MBean from the middle of a hierarchy also triggers a similar reaction up to the top target MBean, which is finally removed. However, as explained in the previous section, corresponding source MBeans that are lower in the hierarchy will not be affected. The cascading service reflects the content of a source MBean server into a target MBean server, but it cannot be used to create or remove MBeans in the source MBean server. Calls to createMBean, registerMBean, and unregisterMBean only affect the local target MBean server.

14.2.2.2 MBean Patterns and Filtering

When the cascading service MBean is instantiated, you can pass it an object name pattern. This object name pattern is applied to the list of MBeans in the subagent's source MBean server to determine those MBeans that are to be cascaded into the master agent's target MBean server. The filtering is activated for the life of the of the mount operation, namely, until unmount is called for that mount point. Care must be taken when using the ObjectName pattern, so as not to implement cascading hierarchies that are not analogous to File System mount operations.

The object name pattern is used to filter any new MBean that is registered in the source MBean server. If the new MBean meets the filter criteria, it will become visible and be cascaded into the target MBean server.

14.2.2.3 Using Target Paths

Although the API also allows you to implement different cascading schemes, your applications should only implement those schemes that can be compared to a regular File System mount, as follows.

The present implementation does not enforce those rules, but applications that are concerned with naming consistency and coherency should make sure to respect them.

14.3 Running the Cascading Example

The examplesDir/current/Cascading directory contains all of the files for the master agent and subagent applications, along with a simple MBean.

To Run the Cascading Example
  1. Compile all files in this directory with the javac command.


    $ cd examplesDir/current/Cascading/
    $ javac -classpath classpath *.java
    
  2. Start the subagent in another terminal window with the following command. Be sure that the classes for the SimpleStandard MBean can be found in its classpath.


    $ java -classpath classpath -Durl="subagent_input_url" SubAgent
    

    Here, subagent_url is the URL that is passed to the JMXConnectorServerFactory to create the subagent's JMXConnectorServer. You can use one of the following URLs:

    • service:jmx:jmxmp://

    • service:jmx:rmi://

    • service:jmx:iiop://

    Alternatively, you can specify your own URL, for example.


    service:jmx:jmxmp://host_name:port_number
    

    Note that you can start many subagents in different terminal windows, for example.


    $ java -Dhtml.port=8082 -Durl="subagent1_input_url" SubAgent
    $ java -Dhtml.port=8182 -Durl="subagent2_input_url" SubAgent
    ...

    However you start the subagent, it will inform you onscreen of the URL, subagent-actual-url, at which it can be found.

  3. Wait for the agent to be completely initialized, then start the master agent with the following command:


    $ java -classpath classpath MasterAgent subagent-actual-url
    

    In the command above, subagent-actual-url is the URL you were given when you launched the subagent. If you started more than one subagent, you can start the MasterAgent as shown in the following command.


    $ java -classpath classpath MasterAgent subagent1-actual-url 
    subagent2-actual-ur ... 
    

    When started, the master agent application first creates the CascadingService MBean and then mounts the subagent's ExportDomain in its target MBean server. The master agent then performs operations on the cascaded MBeans of the subagent. Press Enter to step through the example when the application pauses.

  4. You can interact with the example through the HTML adaptor of the master agent and subagent.

    This is demonstrated in How to Interact with a Cascade Hierarchy.

  5. If you are still receiving timer notifications on the master agent, press Enter once more to remove the listener, but leave both agent applications running.

How to Interact with a Cascade Hierarchy
  1. Open two browser windows side by side and load the following URLs:

    Subagent

    http://subAgent_hostname:8082/

    Master Agent

    http://MasterAgent_hostname:8084/

    In the subagent, you should see the timer MBean in the ExportDomain and a SimpleStandard MBean in the DefaultDomain.

    The master agent is recognizable by the cascading service MBean in com.sun.jdmk. Otherwise it has identical timer MBeans registered in the subagent/agent#/ExportDomain: this is the cascaded version of the timer in the subagent. The SimpleStandard MBean is not cascaded because our cascading service instance filters with the following object name pattern:

    ExportDomain:*

  2. Create four MBeans of the SimpleStandard class in following order:

    On the Master Agent:

    ExportDomain:name=SimpleStandard,number=1

    On the Subagent:

    ExportDomain:name=SimpleStandard,number=1

    ExportDomain:name=SimpleStandard,number=2

    ExportDomain:name=SimpleStandard,number=3

  3. Reload the agent view on the master agent.

    The cascaded MBeans for the last two agents have been created automatically. Look at the MBean view of either of these cascaded MBeans on the master agent. Their class name appears as SimpleStandard.

  4. In the master agent, set a new value for the State string attribute of all 3 of its SimpleStandard MBeans.

    When you look at the corresponding MBeans in the subagent, you see that all the MBeans were updated through the master agent.

  5. In the subagent, invoke the reset operation of all 3 of its SimpleStandard MBeans.

    When you inspect the MBeans in the master agent, the values for all were reset. Remember that the HTML adaptor must get the values of attributes for displaying them, so they were correctly retrieved from the cascaded MBeans that we reset.

  6. In the subagent, unregister MBeans number=1 and number=3, then update the agent view on the master agent.

    The cascaded MBean is automatically removed by the cascading service.

  7. Invoke the stop operation of the CascadingService MBean in the master agent.

    The last cascaded MBean for the timer is removed from the master agent. The two agents are no longer connected.

  8. If you have finished with the agents, press Enter in both of their terminal windows to exit the applications.

Chapter 15 Discovery Service

The discovery service enables you to discover Java dynamic management agents in a network. This service relies on a discovery client object that sends out multicast requests to find agents. In order to be discovered, an agent must have a registered discovery responder in its MBean server. Applications can also use a discovery monitor that detects when discovery responders are started or stopped.

The combination of these functions enables interested applications to establish a list of active agents and keep it up to date. In addition to knowing about the existence of an agent, the discovery service provides the version information from an MBean server's delegate and the list of communication MBeans that are currently registered. The discovery service uses the term communicators to designate a set of MBeans consisting of protocol adaptors and connector server objects.

Often, the discovery client and the discovery monitor are located in a manager application that wants to know about the available agents. However, agent applications are free to use the discovery service because they might require such information for cascading (see “Cascading Agents”) or for any other reason. To simplify using the discovery service in an agent, all of its components are implemented as MBeans.

The code samples in this topic are found in the current/Discovery directory located in the main examplesDir (see “Directories and Classpath” in the Preface).

This chapter covers the following topics:

15.1 Active Discovery

In active discovery, the discovery client initiates searches for agents on the network. It involves a discovery client that sends the discovery request and a discovery responder in each agent that responds. Each instance of the responder supplies the return information about the agent in which it is registered. The return information is represented in the discovery client by a vector of discovery response objects.

The application containing the discovery client can initiate a search at any time. For example, it might do a search when it is first started and periodically search again for information about the communicators that might have changed. For each search, the discovery client broadcasts a request and waits for the return information from any responders.

In the following sections, we describe each of these objects in further detail.

15.1.1 Discovery Client

The DiscoveryClient class provides methods to discover agents. The active discovery operation sends a discovery request to a multicast group and waits for responses. These messages are proprietary and are not exposed to the user. Discovery clients can only discover agents listening on the same multicast group and port, so your design must coordinate this information between the discovery client and responders.

You can instantiate and perform searches from multiple discovery clients in a single application. Each discovery client can be configured to use different multicast groups or ports, enabling you to discover different groups of agents.

The discovery services enable users to specify a local interface from which to send out multicast messages. This is useful when working on a system that has multihome interfaces connecting to disconnected multinetworks. In addition, the DiscoveryResponder constructor enables you to specify the host address to be used to build the discovery response. The address can be specified either as an IPV4 or IPv6 address, or as a host name.


Example 15–1 Instantiating and Initializing a Discovery Client

public class Client {
    public static void main(String[] args) throws Exception {
	       BufferedReader br
	           = new BufferedReader(new InputStreamReader(System.in));
 
	       discoveryClient = new DiscoveryClient();
	       discoveryClient.start();

	       while(true) {
	          echo("\n>>> Press return to discover connector servers;");
	          echo(">>> Type a host name then return to discover connector 
                servers running on that machine;");
	          echo(">>> Type bye then return to stop the client.");

    	    String s = br.readLine();

	          if (s.equalsIgnoreCase("bye")) {
		           System.exit(0);
	          } else {
		           try {
		                discover(s);
		           } catch (Exception e) {
		                echo("Got exception "+e);
		                e.printStackTrace();
		           }
	          }
	       }
    }

Once you have created the discovery client, before initiating searches, you must call the discovery client's start method, as shown in Example 15–1. This will create its multicast socket and join the multicast group used for broadcasting its discovery request. The default multicast group is 224.224.224.224 and the default port is 9000. These can be set to other values through the multicastGroup and multicastPort attributes, but only when the state of the discovery client is OFFLINE.

The scope of the discovery request depends on the time-to-live used by the multicast socket. Time-to-live is defined by the Java class java.net.MulticastSocket to be a number between 1 and 255. By default, the time-to-live is 1, which corresponds to the host's local area network. You can modify this value at any time by setting the discovery client's TimeToLive attribute.

By default, a discovery client waits for responses for one second after it has sent a discovery request. This period can be customized by setting a value in milliseconds for its TimeOut attribute. When setting this attribute, you should take into account estimated time for a round-trip of a network packet using the given time-to-live.

Once started, the discovery client in the example above awaits user inputs before starting searches.

15.1.2 Performing a Discovery Operation

An application triggers a search operation by invoking the findMBeanServers or findCommunicators methods on an active DiscoveryClient object. Using the current settings, it will send the multicast request and block for the timeout period. At the end of the timeout period, these methods return the responses that were received.

Both methods return a vector of DiscoveryResponse objects. This class exposes methods for retrieving information about the MBean server and the registered communicator MBeans in the agent. The MBean server information is the same as that exposed by that agent's MBean server delegate. The communicators are identified by ConnectorAddress objects and indexed by object name in a hash table.

Both search methods return the information about the agent's MBean server. The hash table of communicator MBeans is always empty for discovery responses returned by the findMBeanServers method. Otherwise, you can extract object names and protocol information from the hash table. One way of distinguishing the communicator MBeans is to rely on the default names provided by the ServiceName class.


Note –

All discovery messages sent between components of the discovery service are compatible between applications running different versions of the Java platform or between versions 5.0 and 5.1 of the Java DMK. However, these different configurations are not compatible for subsequent management operations through connectors. You can use the getImplementationVersion method of the DiscoveryResponse object to determine both the Java platform and product version numbers.


In our example, we request all information about the agents and print out all information in the discovery responses.


Example 15–2 Performing a Discovery Operation

private static void discover(String host) throws Exception {
	   Vector v = null;
	   if (host ==  null || host.equals("")) {
	      v = discoveryClient.findCommunicators();
	   } else {
	      v= discoveryClient.findCommunicators(host);
	   }

	   if (v.size() == 0) {
	      echo("No connector server has been found.");

	      return;
	   }

	   for (int i=0; i<v.size(); i++) {
	        DiscoveryResponse dr = (DiscoveryResponse)v.get(i);
	        JMXServiceURL url = null;

	        // legacy servers
	        Collection c = dr.getObjectList().values();
	        for (Iterator iter=c.iterator(); iter.hasNext();) {
		         Object o = iter.next();

		         if (!(o instanceof ConnectorAddress)) {
		             continue;
		         }

		         ConnectorAddress ca = (ConnectorAddress)o;
		         if (ca.getConnectorType().equals("SUN RMI")) {
		             url = new JMXServiceURL("jdmk-rmi",
					                           ((RmiConnectorAddress)ca).getHost(),
					                           ((RmiConnectorAddress)ca).getPort());
		        // Repeat for jdmk-http and jdmk-https connectors 
           [...]
 	        } else {
		            echo("Got an unknown protocol: "+ca.getConnectorType());

		            continue;
		        }

		        echo("\nFound a legacy server which is registered 
                  as a legacy MBean: "
		              +url.getProtocol()+" 
                 "+url.getHost()+" 
                 "+url.getPort());
		        echo("Connecting to that server.");
		        JMXConnector jc = JMXConnectorFactory.connect(url);
		        echo("Its default domain is 
                 "+jc.getMBeanServerConnection().getDefaultDomain());
		        echo("Closing the connection to that server.");
		        jc.close();
	       }

	       // JMX-remote servers
	       JMXServiceURL[] urls = dr.getServerAddresses();

	       echo("");
		    for (int ii=0; ii<urls.length; ii++) {
		      echo("\nFound a server which is registered 
                as a JMXConnectorServerMBean: "
		            +urls[ii]);
		      echo("Connecting to that server.");
		      JMXConnector jc = JMXConnectorFactory.connect(urls[ii]);
		      echo("Its default domain is 
              "+jc.getMBeanServerConnection().getDefaultDomain());
		      echo("Closing the connection to that server.");
		      jc.close();
	       }
	    }
}

On the agent side, the discovery responder automatically replies to discovery requests. Any active, registered responder in the same multicast group that is reached within the given time-to-live of the request will respond. It will automatically gather the requested information about its MBean server and send the response. The settings of the responder do not affect its automatic reply to discovery requests. In 15.2.1 Discovery Responder we will cover how its settings control passive discovery.

The discovery client can search for agents via all the connector protocols supported by Java DMK, both current and legacy. Java DMK 5.1 introduces a new DiscoveryResponse method, getServerAddresses, which is used to get the addresses of any servers registered in an MBean server as a JMXConnectorServerMBean. The getServerAddresses method can discover servers that are either instances of the JMX Remote API JMXConnectorServer, or legacy servers wrapped to appear as such. This new method ensures compatibility between the following pairs of discovery clients and servers created using versions 5.0 and 5.1 of Java DMK.

All the above relations between versions are also true for the passive discovery monitor.

In active discovery, the discovery client controls all parameters of a search it initiates, including the response mode of the discovery responder. The discovery client determines whether responses are sent back on a different socket (unicast) or sent to the same multicast group. The default is unicast: if you want to use the multicast response mode, set the PointToPointResponse attribute to false before initiating the discovery.

15.1.2.1 Unicast Response Mode

When the PointToPointResponse boolean attribute is true, the discovery client specifies unicast mode in its discovery requests. The responder will create a datagram socket for sending the response only to the discovery client. As shown in the following diagram, each responder will send its response directly back to the discovery client. The datagram socket used by each responder is bound to its local host address; this cannot be customized.

Figure 15–1 Unicast Response Mode

Unicast response mode

15.1.2.2 Multicast Response Mode

When the PointToPointResponse boolean attribute is false, the discovery client specifies multicast mode in its requests. The discovery responder will use the existing multicast socket to send response, broadcasting it to the same multicast group as the request. As shown in the following diagram, every member of the multicast group will receive the message, but only the discovery client can make use of its contents. Multicast mode avoids having to open another socket for the response, but all of the responses will create traffic in each application's socket.

Figure 15–2 Multicast Response Mode

Multicast response mode

15.2 Passive Discovery

In passive discovery, the entity seeking knowledge about agents listens for the activation or deactivation of their discovery responders. When discovery responders are started or stopped, they send out a proprietary message that contains all discovery response information. The DiscoveryMonitor object waits to receive any of these messages from the multicast group.

A discovery monitor is often associated with a discovery client. By relying on the information from both, you can keep an up-to-date list of all agents in a given multicast group.

Figure 15–3 Passive Discovery of Discovery Responders

Diagram showing passive discovery of discovery responders

Therefore, configuring and starting the discovery responder is an important step to the overall discovery strategy of your applications.

15.2.1 Discovery Responder

The agents that are configured to be discovered must have an active DiscoveryResponder registered in their MBean server. The responder plays a role in both active and passive discovery:

Both types of messages are proprietary and their contents are not exposed to the user. These messages contain information about the MBean server, its delegate's information and a list of communicator MBeans, unless not requested by the discovery client.

In our example we create the discovery responder in the MBean server and then activate it. Then, we create different connector servers that will discover the agent passively, due to its active discovery responder.


Example 15–3 Creating a Discovery Responder

public class Responder {
    public static void main(String[] args) {
	      try {
	          MBeanServer myMBeanServer = 
                MBeanServerFactory.createMBeanServer();

	          echo("\nCreate and register a DiscoveryResponder MBean.");
	          ObjectName dc = 
               new ObjectName("DiscoveryExample:name=DiscoveryResponder");
          myMBeanServer.createMBean("com.sun.jdmk.discovery.DiscoveryResponder", 
                                    dc);
	          myMBeanServer.invoke(dc, "start", null, null);
	    
	          // Create an HtmlAdaptorServer on the default port.
	          [...]    
	
          // Create JMX Remote API connector servers
	    	    JMXServiceURL url;
	          JMXConnectorServer server;
	          ObjectName on;
	    
	          // rmi
	          url = new JMXServiceURL("rmi", null, 0);
	          server = 
             JMXConnectorServerFactory.newJMXConnectorServer(url, 
                                                            null, 
                                                            myMBeanServer);
	          server.start();
	          url = server.getAddress();
		    
     	    on = new ObjectName("jmx-remote:protocol=rmi");
	          myMBeanServer.registerMBean(server, on);
    	    
          // Create RMI/IIOP connector server
          [...]

          // Create JMXMP connector server
          [...]

          // stop/start the responder to send a notification
	          myMBeanServer.invoke(dc, "stop", null, null);
           Thread.sleep(100);
	          myMBeanServer.invoke(dc, "start", null, null);

          // Create wrapped legacy RMI and HTTP connector servers 
          [...]
	    
	          // Repeat for the other current and legacy connector protocols
          [...]

	          // stop/start the responder to allow a Monitor to find them
	          myMBeanServer.invoke(dc, "stop", null, null);
           Thread.sleep(100);
	          myMBeanServer.invoke(dc, "start", null, null);
           
           [...]

	          echo("All servers have been registered.");

	          echo("\n>>> Press return to exit.");
	          System.in.read();
	          System.exit(0);
	      } catch (Exception e) {
	          e.printStackTrace();
	   }
}

The discovery responder has attributes for exposing a multicast group and a multicast port. These attributes define a multicast socket that the responder will use to receive discovery requests. It will also send activation and deactivation messages to this multicast group. When sending automatic responses to discovery requests, the time-to-live is provided by the discovery client. The responder's time-to-live attribute is only used when sending activation and deactivation messages.

We use the default settings of the discovery responder that are the multicast group 224.224.224.224 on port 9000 with time-to-live of 1. In order to modify these values, you need to set the corresponding attributes before starting the discovery responder MBean. You can also specify them in the class constructor. If the responder is active, you will need to stop it before trying to set any of these attributes. In that way, it will send a deactivation message using the old values and then an activation message with the new values.

15.2.2 Discovery Monitor

The discovery monitor is a notification broadcaster: when it receives an activation or deactivation message from a discovery responder, it sends a discovery responder notification to its listeners. Once its parameters are configured and the monitor is activated, the discovery is completely passive. You can add or remove listeners at any time.

The DiscoveryMonitor MBean has multicast group and multicast port attributes that determine the multicast socket where it will receive responder messages. Like the other components of the discovery service, the default multicast group is 224.224.224.224 and the default port is 9000. You can specify other values for the group and port either in the constructor or through attribute setters when the monitor is off-line.

Example 15–4 shows a discovery monitor being started, and then a listener being added.


Example 15–4 Instantiating and Starting a Discovery Monitor

public class Monitor {
    public static void main(String[] args) throws Exception {

       echo("Create a DiscoveryMonitor.");
	      DiscoveryMonitor dm = new DiscoveryMonitor();
	      dm.start();

	      echo("Add a listener to receive monitor notifications.");
	      dm.addNotificationListener(new MyListener(), null, null);
 
	      echo("Waiting for new server notifications ...");

	      echo("\nType any key to stop the monitor.");
	      System.in.read();

	      dm.stop();
	      echo("\nThe monitor has been stopped.");

	      System.exit(0);
    }

The discovery monitor must be activated with the start operation before it will receive responder messages and send notifications. If it is being used as an MBean, it will be stopped automatically if it is unregistered from its MBean server. If it is not used as an MBean, as is the case here, you should invoke its stop method before your application exits.

15.2.3 Discovery Responder Notifications

When it receives a responder's activation or deactivation message, the discovery monitor sends notification objects of the DiscoveryResponderNotification class. This notification contains the new state of the discovery responder (ONLINE or OFFLINE) and a DiscoveryResponse object with information from the agent where the responder is located.

The listener could use this information to update a list of agents in the network. In our example, the listener is the agent application itself, and the handler method only prints out the information in the notification.


Example 15–5 Discovery Responder Notification Handler

private static class MyListener implements NotificationListener {
	   public void handleNotification(Notification notif, Object handback) {

	      try {
       		DiscoveryResponderNotification dn 
                               = (DiscoveryResponderNotification)notif;
            DiscoveryResponse dr = (DiscoveryResponse)dn.getEventInfo()  ;

		         JMXServiceURL url = null;

		         // legacy servers
		         Collection c = dr.getObjectList().values();
		         for (Iterator iter=c.iterator(); iter.hasNext();) {
		              Object o = iter.next();
		    
		              if (!(o instanceof ConnectorAddress)) {
			               continue;
		              }
		    
		              ConnectorAddress ca = (ConnectorAddress)o;
		              if (ca.getConnectorType().equals("SUN RMI")) {
			               url = new JMXServiceURL("jdmk-rmi",
                             ((RmiConnectorAddress)ca).getHost(),
                             ((RmiConnectorAddress)ca).getPort());
		              // Repeat for jdmk-http and jdmk-https connectors
		              [...]

                  } else {
			 			       continue;
		              }

                  echo("\nFound a legacy server which is registered as
                       a legacy MBean: "
                       +url.getProtocol()+" "+url.getHost()+" "
                       +url.getPort());
		              echo("Connecting to that server.");
                  JMXConnector jc = JMXConnectorFactory.connect(url);
		      		   jc.close();
		         }

		         // JMX Remote API servers
		         JMXServiceURL[] urls = dr.getServerAddresses();
		
		         echo("");
		         for (int ii=0; ii<urls.length; ii++) {
		              echo("\nFound a server which is registered as a 
                      JMXConnectorServerMBean: "
			                +urls[ii]);
		              echo("Connecting to that server.");
		              JMXConnector jc = JMXConnectorFactory.connect(urls[ii]);
		              echo("Its default domain is 
                       "+jc.getMBeanServerConnection().getDefaultDomain());
		      	      jc.close();
		         }
	        } catch (Exception e) {
		         echo("Got unexpected exception: "+e);
		         e.printStackTrace();
	        }
      }
    }

15.3 Running the Discovery Example

The examplesDir/current/Discovery directory contains the source file for the application that demonstrates the discovery service.

To Run the Discovery Example
  1. Compile the Java classes.


    $ javac -classpath classpath *.java
    
  2. Start the Client.


    $ java -classpath classpath Client
    

    You will be prompted to press Enter to discover connector servers, to provide the host name of a machine where servers are running or to exit the discovery component. However, there are not yet any servers to discover, because the Responder has not yet been started.

  3. In a second terminal window, start the Monitor


    $ java -classpath classpath Monitor
    

    You will see confirmation that the monitor has started, and that it is passively listening for notifications,

  4. In a third terminal window, start the Responder.


    $ java -classpath classpath Responder
    

    You will see the creation of the different connector servers. Then you will be invited to create further connector servers by pressing Enter.

    In the window in which you started the Monitor, you will see the connector servers created by Responder being discovered.

  5. Press Enter in the terminal window in which you started the Client.

    You will see the Client discovering the connector servers created by the Responder, and connecting to them to retrieve information about them, before closing the connections.

    You can continue discovering and connecting to connector servers for as long as the Responder is active.

  6. When you have finished, stop the Responder, the Monitor and the Client.