The Sample Producer/Consumer Application

In the remainder of this chapter, we will build and run a sample application that demonstrates each MBean instrumentation approach. The sections that follow look at the design of the application, where to obtain the source code, how to actually build and run the application, and how to monitor the application via a web browser.

Design

In this section, we will take a look at how the sample application is designed, so that you can better understand what is going on when you see it run. First, we will look at the pattern that is fundamental to the application’s design. Then we will see how the pattern is implemented and what classes constitute the source code for the application.

The design pattern used in the application is a monitor. A monitor is a construct that coordinates activity between multiple threads in the system. In this pattern, the monitor coordinates activities between two categories of threads: producer threads and consumer threads. As you might imagine, a producer thread provides something that the consumer uses. That “something” is generically defined as a unit of work. This can be physically realized as anything relevant to a problem that is solved by this pattern.

For example, the unit of work might be an email message that is sent to the email system (the monitor) by the producer (an email client) and removed by the consumer (some agent on the incoming email server side). The producer might perform additional processing on the message before sending it to the email system, such as checking the spelling. By the same token, the consumer may perform additional processing of the message after removing it from the queue, such as applying an anti-virus check. For this reason, we will refer to the pattern as “value-added producer/consumer.” This pattern is shown in UML notation in Figure 1-9.

UML diagram showing the “value-added producer/consumer” pattern

Figure 1-9. UML diagram showing the “value-added producer/consumer” pattern

As you can see in Figure 1-9, the producer and consumer are separated (decoupled) by the monitor. This pattern is best applied to systems that are inherently asynchronous in nature, where the producer and consumer are decoupled by varying degrees. This decoupling can be a separation of location as well as of synchronicity.

The implementation of the value-added producer/consumer pattern is shown in Figure 1-10. The classes in the diagram are implemented as Java classes. The stereotypes shown in the diagram are named according to the pattern shown in Figure 1-9.

UML diagram showing the implementation of the pattern in the form of the application

Figure 1-10. UML diagram showing the implementation of the pattern in the form of the application

Basic is the base class for all of the classes that make up the implementation (with the exception of WorkUnit, which represents the unit of work that is exchanged between Supplier and Consumer). Controller is a class that acts as the JMX agent and is responsible for creating the producer and consumer threads that run inside the application. Queue is a thread-safe queue that acts as the monitor. Producer threads place items in the queue in a thread-safe way, and consumer threads remove them. Worker is the base class for Supplier and Consumer, because much of their behavior is common.

In the sample application, the following resources can be managed:

  • Controller

  • Queue

  • Supplier

  • Consumer

I encourage you to look at the source code to see exactly what attributes and operations are on each of the management interfaces for these resources.

Source Code

The source code for the application is standalone with respect to each type of instrumentation approach. There are three versions of the application, each in its own package. The name of the package corresponds to the instrumentation approach. For example, with the exception of common classes such as GenericException, the application source code for standard MBeans is entirely contained in the standard package; thus, if you install the source code to c:\jmxbook, the path to the application source code for standard MBeans will be c:\jmxbook\sample\standard. All of the source code shares the contents of the exception package. Other than that, however, the application can be built and run independently of the other packages.

For each type of MBean, there is a Windows batch file and a Unix (Korn shell) script that builds and runs the code for that instrumentation strategy. The name of the script or batch file matches the instrumentation strategy: for example, the build script for dynamic MBeans is called dynamic.sh, and the batch file for building the source code for the version of the application instrumented as dynamic MBeans is called dynamic.bat. The major differences between the application versions are in the source code. The console output and the management view will show very little difference (other than output from the Ant build script) between the versions of the application.

Building and Running the Application

Before you can build and run the sample application (see Section P.5 in the Preface for details on how to obtain the application’s source code), you must download the JMX RI and Jakarta Ant. For this book, I used JMX RI 1.0.1 and Ant 1.4. You can obtain the JMX RI at http://java.sun.com/products/JavaManagement/ and Jakarta Ant at http://jakarta.apache.org/ant/index.html.

The name of the build file Ant uses to build the application for all of the instrumentation strategies is build.xml. The build scripts are designed to work with very little modification on your part. However, you may have to modify either the build script or the Ant build file, depending on where you installed the JDK, the JMX RI, and Ant itself. Example 1-5 shows an excerpt from build.xml.

Example 1-5. Selected portions of the Ant build file for the application, build.xml

.
.
.
<project name="jmxbook" default="standard" basedir=".">
  
<!-- Set global properties -->
<property name="source_root" value="c:\jmxbook\sample"/>
<property name="jmx_home" value="c:\jmx1.0.1"/>
  
<path id="project.general.class.path">
  <pathelement path="${jmx_home}\jmx\lib\jmxri.jar"/>
  <pathelement path="${jmx_home}\jmx\lib\jmxtools.jar"/>
  <pathelement path="."/>
</path>
  
<!-- Build the init target -->
<target name="init">
  <!-- create the time stamp -->
  <tstamp>
    <format property="build.start.time" pattern="MM/dd/yyyy hh:mm:ss aa"/>
  </tstamp>
  <echo message="Build started at ${build.start.time}..."/>
</target>
  
<!-- Build the exception target -->
<target name="build-exception" depends="init">
  <javac>
    <classpath refid="project.general.class.path"/>
    <src path="${source_root}"/>
    <include name="exception\*"/>
  </javac>
</target>
  
<!-- Build the "standard" target -->
<target name="build-standard" depends="build-exception">
  <javac>
    <classpath refid="project.general.class.path"/>
    <src path="${source_root}"/>
    <include name="standard\*"/>
  </javac>
</target>
  
<!-- Build the "dynamic" target -->
<target name="build-dynamic" depends="build-exception">
  <javac>
    <classpath refid="project.general.class.path"/>
    <src path="${source_root}"/>
    <include name="dynamic\*"/>
  </javac>
</target>
  
<!-- Build the "model" target -->
<target name="build-model" depends="build-exception">
  <javac>
    <classpath refid="project.general.class.path"/>
    <src path="${source_root}"/>
    <include name="model\*"/>
  </javac>
</target>
.
.
.
</project>

As you can see, the Ant build file is an XML document. This is what sets Ant apart from other build utilities, such as make. Each component to be built using Ant is called a target. A target may have one or more dependent targets that must be built first, each of which may be dependent on other targets, and so on. Ant resolves these dependencies for you. A target is specified in an Ant build file as an XML tag called target and has the following format:

<target name="mytarget" depends="d1,d2">

in which case mytarget depends on targets d1 and d2, or:

<target name="mytarget">

if mytarget has no dependent targets. Let’s look at the build-standard target from Example 1-5:

<!-- Build the "standard" target -->
<target name="build-standard" depends="build-exception">
  <javac>
    <classpath refid="project.general.class.path"/>
    <src path="${source_root}"/>
    <include name="standard\*"/>
  </javac>
</target>

You can see that the build-standard target depends on the build-exception target. Ant knows that there may be other dependencies, so it looks at build-exception:

<!-- Build the exception target -->
<target name="build-exception" depends="init">
  <javac>
    <classpath refid="project.general.class.path"/>
    <src path="${source_root}"/>
    <include name="exception\*"/>
  </javac>
</target>

and notices that build-exception depends on init. Ant then looks at init:

<target name="init">
  <!-- create the time stamp -->
  <tstamp>
    <format property="build.start.time" pattern="MM/dd/yyyy hh:mm:ss aa"/>
  </tstamp>
  <echo message="Build started at ${build.start.time}..."/>
</target>

Ant sees that init has no dependencies, so it begins the build. init is built first, followed by build-exception and finally build-standard. Notice the javac tag within build-standard and build-exception. This is known as an Ant task. A task is a Java class that executes within the JVM in which Ant is running (unless you tell Ant to fork a new process when executing the task). The javac task is the java compiler. The classpath, src, and include tags nested within the javac task tell the Java compiler what the CLASSPATH is, the root location of the .java files, and the packages (directories) to compile, respectively.

The application classes for each chapter in this book are built and run using either a batch file or a shell script. If you are running the application on Windows (as I did to produce the screen shots for this chapter), use the batch file (i.e., the .bat file). If you are running the application on Unix, use the shell script (i.e., the .sh file). Throughout the rest of this chapter, the examples will be Windows-based. There are two reasons for this. First, because of the popularity of Windows, it is likely that most developers will be running this operating system. Second, the differences in the behavior of the application when it is run on Windows versus Unix are negligible.

To build and run the application, type in the name of the batch file you want to run, based on the type of MBean instrumentation strategy you want to see in action. You will notice that there is no detectable difference between what you see when you run the build/run batch file and what you see in your browser (discussed in the next section), regardless of the instrumentation strategy. Suppose we want to run the standard MBean batch file, which will build and run the application as standard MBeans. Example 1-6 shows the batch file that builds the application.

Example 1-6. standard.bat, the batch file that builds the application as standard MBeans

@set TARGET_NAME=build-standard
@set JAVA_HOME=c:\jdk1.3.1
@set ANT_VERSION=1.4
@set ANT_HOME=c:\ant%ANT_VERSION%
  
@echo Starting Build ...
  
call %ANT_HOME%\bin\ant %TARGET_NAME%
  
if NOT "%ERRORLEVEL%"=="0" goto DONE
  
%JAVA_HOME%\bin\java sample.standard.Controller 100 150
  
:DONE

This batch file is very simple. Aside from setting a few environment variables, it does only two things: it builds the application by calling Ant, and, if that succeeds, it starts the application. Figure 1-11 shows the output of running the batch file. Recall our earlier discussion of how Ant resolves target dependencies; you’ll see that the targets are built in the order described there.

Running the build/run batch file for standard MBeans

Figure 1-11. Running the build/run batch file for standard MBeans

All of the batch files (standard.bat, dynamic.bat, and model.bat) operate as described below, but I’ve used standard.bat here for the purposes of illustration.

In each version of the application, Controller contains the main( ) method that starts the producer and consumer threads and is itself an MBean that can be managed and monitored. There are two command-line arguments to Controller’s main( ) method: the work factor for the producer thread and the work factor for the consumer thread. Notice that in standard.bat values of 100 and 150, respectively, are specified for these arguments. I set these values for a reason: it is unlikely that you will find an application of the value-added producer/consumer pattern where the producer and consumer perform an equal amount of work. These command-line parameters to Controller allow you to simulate this asymmetry. When Controller is started, one producer thread and one consumer thread are created. However, Controller has a management method that allows you to start additional threads to balance out the workload (we will see how to do this later).

Figure 1-10 illustrates the relationship between the various classes in the application, where there is a single Queue object into which Supplier threads place WorkUnit objects and from which Consumer threads remove them. For a single unit of work, here is the flow of control:

  1. The Supplier performs an amount of work N—where N is specified on the command line to Controller—and places a single WorkUnit object into the Queue.

  2. The Consumer removes a single WorkUnit object from the Queue and performs an amount of work M—again, where M is specified on the command line to Controller.

These steps are repeated for each work unit.

Tip

The work that is performed by Supplier and Consumer threads is to calculate prime numbers. The amount of work specified on the command line to Controller is the number of prime numbers to calculate for each WorkUnit. The Supplier calculates N primes, then places a WorkUnit object into the Queue. The Consumer removes a WorkUnit object from the Queue and then calculates M primes.

This section looked at how to run the sample application and briefly discussed what it is doing internally to simulate the production and consumption of units of work. I strongly encourage you to examine the source code for yourself to see the various attributes and operations available on the management interfaces of each resource in the application.

In the next section, we will look at how to use a web browser to monitor and manage the sample application’s MBeans.

Monitoring and Managing the Application

Once the application is running, you can point your web browser to port 8090 (the default—you can change this, but if you do so, remember to point your browser to the new port number). Figure 1-12 shows the result of pointing my web browser (which happens to be Internet Explorer) to port 8090 after running standard.bat.

The management view of the application in Internet Explorer

Figure 1-12. The management view of the application in Internet Explorer

Remember the work factors that we specified on the command line to Controller for the producer and consumer threads? Because they are different (100 and 150, respectively), and the producer thread does less work than the consumer thread for each work unit, I expect the Queue to always be full once the application reaches a steady state.

If I click on the Queue MBean in my browser, I see the screen shown in Figure 1-13. There are several interesting things about Figure 1-13. First, the AddWaitTime attribute is much larger than the RemoveWaitTime attribute. After processing 72 units of work (according to the NumberOfItemsProcessed attribute), the Supplier thread has waited a total of 3,421 milliseconds to add items to the Queue because it was full, whereas the Consumer thread has not had to wait at all to remove items (although, depending on which thread actually starts first, you may see a small amount of Consumer wait time). This is pretty much what we would expect, as the Supplier thread does only two-thirds the work of the Consumer thread.

The management view of the Queue object

Figure 1-13. The management view of the Queue object

Suppose we want to start another Consumer thread to pick up some of the slack of the other Consumer thread and balance things out a bit. For the moment, let’s ignore the fact that we can control the amount of work each type of Worker thread can perform. In a real-world application, we would not have that luxury. As I mentioned earlier in this chapter, Controller acts as the JMX agent for the application, but it is also itself a managed resource (i.e., an MBean). If we look at the management interface of Controller, we’ll see that there is a management operation to start new Worker threads, called createWorker( ). Figure 1-14 shows the management view of the Controller MBean and its createWorker( ) operation.

The management view of Controller showing the createWorker( ) operation

Figure 1-14. The management view of Controller showing the createWorker( ) operation

There are two parameters to createWorker( ): the first is a string that contains the worker type, and the second is the work factor that worker is to have (i.e., the number of primes calculated per unit of work). The valid values for the worker type are “Supplier” and “Consumer”. We want to create a new Consumer thread with the same work factor as the currently running Consumer thread, so we set these parameters to Consumer and 150, respectively. Once we have entered the parameters for the management operation into the text boxes, as shown in Figure 1-14, we click the createWorker button to invoke the management operation. If the operation succeeds, we will see a screen that looks like Figure 1-15.

The screen we see once createWorker( ) has successfully been invoked

Figure 1-15. The screen we see once createWorker( ) has successfully been invoked

We would now expect that activity in the Queue has balanced out somewhat, and we would expect to start seeing the Supplier wait, as we now have two Consumer threads at work. Figure 1-16 shows the management view of the Queue after we start the second Consumer thread.

The management view of the Queue after starting a second Consumer thread

Figure 1-16. The management view of the Queue after starting a second Consumer thread

Notice that after processing 1,013 units of work (as we see from the NumberOfItemsProcessed attribute), the Consumer threads have waited nearly 7 times as long as the Supplier thread. Through the use of management operations, we can give an operator at a management console the ability to tune our application at runtime.

Get Java Management Extensions now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.