Client quality reporting for J2EE Web services

Use SOAP attachments to report client response times for Web services

Web services are fast becoming the preferred architecture for implementing client-server systems. The advantage of Web services is that a business can formally define a set of services, and then generate the entire client and server codebase for communications, thus simplifying new client access to legacy Web resources.

However, while Web services ease the building of client-server systems, monitoring service quality is a significant problem. Consider a client application that submits a transaction on a user's behalf. A business transaction usually involves several Web service calls: an initial call to submit a work item, subsequent calls to check for completion, and a final call to get the result. Each call is a distinct HTTP/SOAP (Simple Object Access Protocol) exchange. Put yourself in the position of an IT department responsible for monitoring server load and forecasting future needs. The fundamental question you must answer is, "How well am I serving my clients now, and what will I need to serve them in the future?"

Answering this question is difficult if you have only HTTP logs. Clients care about transactions, but since each transaction consists of several HTTP requests, the best you can do to estimate service quality is to develop custom data-mining software that cursors through HTTP logs and builds a model of user transactions. Even so, the information you have is still limited because it can't reflect network transport or client application overhead.

This article's key idea is that transaction service quality is best measured by the client. The approach adopted here allows the client to record actual transaction response times. A client application uploads response time reports to the server by appending them to the next up-bound transaction request. The server strips off these attachments and queues them for storage and offline analysis.

Architecture

One objective of the client-based metrics-recording architecture is that the recording infrastructure must be lightweight, both in terms of runtime overhead and the ease of adding it to an existing implementation. We also want an architecture that places no constraints on the services offered—we'd like to be able to add it to an existing client-server system that uses Web services as easily as possible.

Another objective of our architecture is that we don't want to make the business application itself less reliable. We'll be introducing some new, lightweight steps into the application process workflow. We must ensure any failures in these new steps are handled because we don't want a business transaction to fail just because we couldn't gather metrics on it.

The following diagram shows a typical J2EE (Java 2 Platform, Enterprise Edition) Web service client-server application. Typical components appear in black; the new components that we will add for metrics gathering appear in red.

J2EE Web services: Metrics-gathering architecture

The "J2EE Application Server" region represents existing server resources. These are the Enterprise JavaBeans (EJB) components that process client requests. A tool automatically generates the Web services package. The EJB components and the associated Web services module deploy to a J2EE server as a J2EE application. When the application deploys, a client can determine available services by invoking the application WSDL (Web Services Description Language) service, which provides a formal definition of the services offered by the application.

The "Application Client" region is composed of an application component and a Web services package. The application component implements business logic and a user interface. The Web services package is generated automatically through a WSDL compiler and the WSDL service provided by the J2EE server application.

Conceptually, the overall system has two layers. The application layer has EJB components on the server side, an application on the client side. The Web services layer has a server implementation and a client implementation, both of which generate automatically.

Typically, a business transaction by a user consists of many server calls. The first initiates a transaction, returning a "handle" to the client. Subsequent calls inquire about transaction completion—the client calls a service with the handle to check if the transaction has finished. Usually a final call obtains a completed transaction's status. Thus, a business model, implemented within the client application, relates a transaction to low-level server calls.

We can add the metrics-gathering components into our standard J2EE Web services architecture. The figure's Payload package deploys on both the server and client. I'll provide more details about this package later, but in an architectural sense, it offers several services. For example, client applications can use beginTransaction() and commitTransaction() to delimit transactions and record elapsed times. The client Web services package uses the Payload package to serialize a metrics report to a SOAP message attachment. The server Web services package uses the Payload package to strip a SOAP attachment from an incoming request, and queue it for logging and reporting purposes.

There is little overhead in this implementation because the client and server exchange no new traffic—a metrics report on a completed transaction rides along with the next client request. The only new processing introduced is some serialization on the client and attachment queuing on the server. The implementation is lightweight because only a single line of code is added to each application Web method, and this code is always the same—it doesn't change if the Web method signature changes.

The last new component introduced is a message-driven EJB component that reads serialized metrics attachments. Typically these would be logged to a database so the enterprise can maintain a historical record of transaction service quality. The enterprise can use this database to relate actual transaction response times to server resource utilization, gaining critical insight into which server components are the key service bottlenecks. Since attachments are queued, the metrics-reader EJB component should run on a different J2EE server instance because we don't want metrics logging to compete for resources with business EJB components.

Implementation

In this section, I show how the metrics code integrates with a simple J2EE client-server application. All code is available for download from Resources; the following section shows how to build and run this code with Sun ONE (Open Network Environment) Application Framework.

Application server prototype

In our example, the server application consists of a single session bean. There is no loss of generality here because the internal server EJB design doesn't affect the metrics-recording architecture. The same metrics approach can be used even if many different EJB components exist in the server.

The XactBean EJB exposes three business methods: submitWork(), checkWork(), and getResult(). Each is a distinct Web method. The client application uses all of them to model a client that submits multiple Web requests to execute a user transaction.

The session bean that represents the server application is shown below:

package TransactionProcessor;
import javax.ejb.*;
import java.rmi.server.*;
import java.util.*;
/**
 * Created May 19, 2003 10:07:39 PM
 * Code generated by the Sun ONE Studio EJB Builder
 * @author Brian Connolly brian@ideajungle.com
 */
public class XactBean implements javax.ejb.SessionBean {
    private javax.ejb.SessionContext context;
    
    private int mRandom;    
    
    /**
     * @see javax.ejb.SessionBean#setSessionContext(javax.ejb.SessionContext)
     */
    public void setSessionContext(javax.ejb.SessionContext aContext) {
        context=aContext;
    }
    
    
    /**
     * @see javax.ejb.SessionBean#ejbActivate()
     */
    public void ejbActivate() {
        
    }
    
    
    /**
     * @see javax.ejb.SessionBean#ejbPassivate()
     */
    public void ejbPassivate() {
        
    }
    
    
    /**
     * @see javax.ejb.SessionBean#ejbRemove()
     */
    public void ejbRemove() {
        
    }
    
    
    /**
     * See section 7.10.3 of the EJB 2.0 specification
     */
    public void ejbCreate() {
        Random r = new Random();
        
        mRandom = r.nextInt(10000);
    }
    
    public java.lang.String SubmitWork(java.lang.String Work) {
        return new Integer(mRandom).toString();
    }
    
    public boolean CheckWork(java.lang.String Xact) {
        return true;
    }
    
    public java.lang.String GetResult(java.lang.String Xact) {
        return new Integer(mRandom).toString();
    }
    
}

These three methods model a client transaction. In submitWork(), we generate a handle as a random number—in reality this would be a unique transaction identifier. checkWork() always returns true. In a real system, the client would pass a transaction identifier, and this method would check with a backend transaction manager to see if the transaction was finished. Similarly, in a real system, getResult() would return a complex transaction completion record.

Server Web services package

The server Web services package generates automatically. In Sun ONE Studio, a Web module can be created by selecting a set of EJB Java methods, and the Web services package classes can generate from this module.

The package consists of many classes and interfaces. The key item here is the <ServiceName>ServantInterface_Tie class, where <ServiceName> is the name of the service. The Tie class is the Web services module's uppermost stack; it binds the incoming service invocation to the EJB component it was generated from. We need to modify only this generated class to add metrics recording.

Tie contains many methods, but the only ones we modify are the ones associated with the EJB business methods: invoke_<X>, where <X> is the name of the EJB business method. We add an import Payload.*; to Tie, and we make a small modification to each business method. Let's look at the invoke_SubmitWork() method:

/*
  * This method does the actual method invocation for operation: SubmitWork
  */
 private void invoke_SubmitWork(StreamingHandlerState state) throws Exception {
    TransactionService.XactServiceGenServer.
      XactServiceServantInterface_SubmitWork_RequestStruct 
         myXactServiceServantInterface_SubmitWork_RequestStruct = null;
    Object myXactServiceServantInterface_SubmitWork_RequestStructObj =
    state.getRequest().getBody().getValue();
   
    /* Line added to generated method: */
    Serializer.queueFirstAttachmentText(state.getMessageContext());
    if (myXactServiceServantInterface_SubmitWork_RequestStructObj 
      instanceof SOAPDeserializationState) {
       myXactServiceServantInterface_SubmitWork_RequestStruct =
         (TransactionService.XactServiceGenServer.
            XactServiceServantInterface_SubmitWork_RequestStruct)
               ((SOAPDeserializationState)
                  myXactServiceServantInterface_SubmitWork_RequestStructObj)
                     .getInstance();
    } else {
       myXactServiceServantInterface_SubmitWork_RequestStruct =
       (TransactionService.XactServiceGenServer.
         XactServiceServantInterface_SubmitWork_RequestStruct)
            myXactServiceServantInterface_SubmitWork_RequestStructObj;
    }
    java.lang.String result =
    ((TransactionService.XactServiceGenServer.XactServiceServantInterface) 
      getTarget()).SubmitWork
         (myXactServiceServantInterface_SubmitWork_RequestStruct.getString_1());
    TransactionService.XactServiceGenServer.
      XactServiceServantInterface_SubmitWork_ResponseStruct 
         myXactServiceServantInterface_SubmitWork_ResponseStruct =
            new TransactionService.XactServiceGenServer
               .XactServiceServantInterface_SubmitWork_ResponseStruct();
    SOAPHeaderBlockInfo headerInfo;
    myXactServiceServantInterface_SubmitWork_ResponseStruct.setResult(result);
    SOAPBlockInfo bodyBlock = new SOAPBlockInfo
      (ns1_SubmitWork_SubmitWorkResponse_QNAME);
    bodyBlock.setValue(myXactServiceServantInterface_SubmitWork_ResponseStruct);
    bodyBlock.setSerializer
      (myXactServiceServantInterface_SubmitWork_ResponseStruct_SOAPSerializer);
    state.getResponse().setBody(bodyBlock);
 }

We added a single line to invoke_SubmitWork():

Serializer.queueFirstAttachmentText(state.getMessageContext());

getMessageContext() returns an object implementing the javax.xml.rpc.handler.soap.SOAPMessageContext interface. This object provides access to the current SOAP message. We pass the object implementing the SOAPMessageContext interface to a static method in Payload.Serializer. The static method gets the XML string from the first message attachment and queues it for the metrics-handler EJB component.

We make identical changes to each of the invoke_<X> methods.

The Payload package

The Payload package is used on both the client and server. It consists of three classes: ClientReport, CurrentReport, and Serializer.

The ClientReport represents a client metrics report:

package Payload;
import java.io.*;
import java.util.*;
/**
 *
 * @author  Brian Connolly Brian@ideajungle.com
 */
public class ClientReport implements Serializable {
   
   public Date clientStartDateTime;
   public Date serverStartDateTime;
   public long clientElapsedMS;
   public String type;
   public String status;
   public String transactionID;
   public String clientID;
   //Default public constructor for WSDL
   public ClientReport() {
   }
/*
 . . . Get, set property methods are not shown
*/

In the code above, clientStartDateTime records the time the client initiated a transaction. serverStartDateTime isn't used currently; its purpose is to hold a transaction's server start time so transaction times can be related to a time history of server resource utilization.

clientElapsedMS is the major metric we're recording: the elapsed number of milliseconds from the time the client begins recording a new transaction until the time it receives results from the final Web service call.

type allows the client to characterize transaction by type. Normally, transaction systems offer many different types of transactions. We expect some to be relatively easy for the server, some to be relatively hard, and we want to distinguish these when analyzing response times and measuring server resources.

status records a finished transaction's completion status.

ClientID is a client identifier. We can use it to distinguish successive transactions by the same client when analyzing quality of service.

The client uses the second class, CurrentReport, to delimit application transactions:

package Payload;
import java.util.*;
import java.rmi.server.*;
/**
 *
 * @author  Brian Connolly Brian@ideajungle.com
 */
public class CurrentReport {
   
   public static UID ClientIdentifier = new UID();
   /** Holds value of property currentReport */
   public static ClientReport Report;
   public static ClientReport LastReport;
   
   /** Creates a new instance of CurrentReport */
   public CurrentReport() {
   }
   
   public void BeginTransaction() {
      Report = new ClientReport();
      Report.setClientID(ClientIdentifier.toString());
      Report.setClientStartDateTime( new Date());
   }
   
   public void CommitTransaction
      (String transactionID, String type, String status) {
      Report.setTransactionID(transactionID);
      Report.setStatus(status);
      Report.setType(type);
      long l1 = Report.getClientStartDateTime().getTime();
      long l2 = new Date().getTime();
      Report.setClientElapsedMS(l2-l1);
      LastReport = Report;
      Report = null;
      
   }
   /** Getter for property currentReport
    * @return Value of property currentReport
    */
   public static ClientReport getReport() {
      ClientReport last = LastReport;
      LastReport = null;
      return last;
   }
   
   /** Setter for property currentReport
    * @param currentReport New value of property currentReport
    */
   public void setReport(ClientReport Report) {
      this.LastReport = Report;
   }
   
}

CurrentReport maintains the current ClientReport of a transaction in progress. It also holds LastReport, the most recently completed transaction. It generates a client identifier as a unique machine identifier—in practical use, this client identifier should be modified to be a globally unique identifier. CurrentReport is not thread safe; we assume only a single thread in the client application executes server transactions.

beginTransaction() creates a new ClientReport, sets its client identifier, and records the transaction start time. commitTransaction() computes the transaction's elapsed milliseconds and saves a copy of the final report for later upload to the server.

Serializer is the third class in the Payload package. Both the client and server use this class. The client uses attachPendingReportToMessage() to serialize a pending ClientReport as XML and add it as a text attachment to a SOAP message. The server uses queueFirstAttachmentText() to strip off the message attachment and queue it for consumption by the message-driven EJB component that handles metrics reports:

package Payload;
import java.io.*;
import java.util.Iterator;
import java.beans.*;
import javax.xml.rpc.handler.soap.*;
import javax.xml.soap.*;
import javax.jms.*;
import javax.naming.*;
/**
 *
 * @author  Brian Connolly Brian@ideajungle.com
 */
public class Serializer {
   
   // Common queue connections used by queueFirstAttachment
   private static Context jndiContext = null;
   private static QueueConnectionFactory queueConnectionFactory = null;
   private static QueueConnection queueConnection = null;
   private static QueueSession queueSession = null;
   private static Queue queue = null;
   private static QueueSender queueSender = null;
   private static boolean connectionEstablished = false;
   
   
   /** Creates a new instance of Serializer */
   public Serializer() {
   }
   private static String ClientReportXML(ClientReport r) {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      XMLEncoder sstream = new XMLEncoder(baos);
      
      sstream.writeObject(r);
      sstream.flush();
      sstream.close();
      return baos.toString();
   }
   
   public static ClientReport ClientReportXML(String crXML) {
      ByteArrayInputStream bais = new ByteArrayInputStream(crXML.getBytes());
      XMLDecoder sstream = new XMLDecoder(bais);
      
      return (ClientReport)sstream.readObject();
   }
   
   public static void attachPendingReportToMessage(SOAPMessageContext smc) {
      try{
         ClientReport cr = CurrentReport.getReport();
         if (cr != null)  {
            SOAPMessage  mc = smc.getMessage();
            AttachmentPart ap = mc.createAttachmentPart
            (Serializer.ClientReportXML(cr),new String("text/plain"));
            mc.addAttachmentPart(ap);
         }
      }
      catch(Exception e){
         // Make sure that application processing proceeds undisturbed
      }
   }
   
   public static void queueFirstAttachmentText(SOAPMessageContext smc) {
      String sattachment;
      try{
         SOAPMessage  mc = smc.getMessage();
         Iterator attachments = mc.getAttachments();
         if(attachments.hasNext()){
            AttachmentPart attachment = (AttachmentPart)attachments.next();
            sattachment = attachment.getContent().toString();
            attachments.remove();
            queueReportXML(sattachment);
         }
      }
      catch (SOAPException e){
         System.out.println("Queue Attachment exception:" + e.toString());
      }
   }
   
/*
Synchronized because all callers share the queue resources
 */
   public static synchronized void queueReportXML(String clientReportXML) {
      TextMessage message = null;
      if (!connectionEstablished) {
         try{
            jndiContext = new InitialContext();
            queueConnectionFactory = 
               (QueueConnectionFactory) jndiContext.lookup(
                     "jms/TestMDBFactory");
            System.out.println("have factory");
            queue = (Queue) jndiContext.lookup("jms/TestMDBQueue");
            
            queueConnection = queueConnectionFactory.createQueueConnection();
            queueSession = queueConnection.createQueueSession
               (false, Session.AUTO_ACKNOWLEDGE);
            queueSender = queueSession.createSender(queue);
            connectionEstablished = true;
         }
         catch (Exception e){
            System.out.println("Exception occurred connecting to queue: "
            + e.toString());
         }
      }
      try {
         message = queueSession.createTextMessage();
         message.setText(clientReportXML);
         //System.out.println("Sending message: " + (String)message.getText());
         queueSender.send(message);
      } catch (JMSException e) {
         System.out.println("Exception occurred: " + e.toString());
      } catch (Exception e){
         // Make sure that application processing proceeds undisturbed
      }
   }
   
}

Note that queueReportXML() is synchronized. Since the Serializer class maintains a single, static queue connection, we must ensure that only one execution thread at a time uses the connection.

Note also that this implementation assumes it is the only producer and consumer of HTTP/SOAP message attachments. If an implementation uses attachments for other purposes, the Serializer class needs to be modified to mark and retrieve the specific attachment that contains a message report.

One of our objectives with this architecture is to ensure that failure to handle metrics reports does not affect completion of a business application. Each method exposed by the Serializer package catches any exceptions that arise so transaction processing can proceed regardless of these errors.

Client services packages

The client services package is generated automatically by directing a WSDL compiler to the WSDL supplied by the application server. Many classes are in this package. The only one we modify is the client Stub class. In the same way that the server Web service package has a Tie class that binds Web service requests to EJB methods, the client Stub class offers a client a method for each service business method.

The service Stub class is named <ServiceName>ServantInterface_Stub, where <ServiceName> is the service's name. Let's look at the submitWork() method within the XactServiceServantInterface_Stub class:

/*
  *  Implementation of submitWork
  */
 public java.lang.String submitWork(java.lang.String string_1)
 throws java.rmi.RemoteException {
    try {
       StreamingSenderState _state = _start(_handlerChain);
       InternalSOAPMessage _request = _state.getRequest();
       _request.setOperationCode(SubmitWork_OPCODE);
       Xact.XactServiceServantInterface_SubmitWork_RequestStruct 
          _myXactServiceServantInterface_SubmitWork_RequestStruct =
       new Xact.XactServiceServantInterface_SubmitWork_RequestStruct();
       _myXactServiceServantInterface_SubmitWork_RequestStruct
           .setString_1(string_1);
       SOAPBlockInfo _bodyBlock = 
           new SOAPBlockInfo(ns1_SubmitWork_SubmitWork_QNAME);
       _bodyBlock.setValue
        (_myXactServiceServantInterface_SubmitWork_RequestStruct);
       _bodyBlock.setSerializer(myXactServiceServantInterface_SubmitWork_
          RequestStruct_SOAPSerializer);
       _request.setBody(_bodyBlock);
       _state.getMessageContext().setProperty
          (HttpClientTransport.HTTP_SOAPACTION_PROPERTY, "");
       Serializer.attachPendingReportToMessage(_state.getMessageContext());
       _send((String) _getProperty(ENDPOINT_ADDRESS_PROPERTY), _state);
       Xact.XactServiceServantInterface_SubmitWork_ResponseStruct 
          _myXactServiceServantInterface_SubmitWork_ResponseStruct = null;
       Object _responseObj = _state.getResponse().getBody().getValue();
       if (_responseObj instanceof SOAPDeserializationState) {
          _myXactServiceServantInterface_SubmitWork_ResponseStruct =
          (Xact.XactServiceServantInterface_SubmitWork_ResponseStruct)
             ((SOAPDeserializationState)_responseObj).getInstance();
       } else {
          _myXactServiceServantInterface_SubmitWork_ResponseStruct =
          (Xact.XactServiceServantInterface_SubmitWork_ResponseStruct)
                _responseObj;
       }
       return _myXactServiceServantInterface_SubmitWork_ResponseStruct
           .getResult();
    } catch (RemoteException e) {
       // Let this one through unchanged
       throw e;
    } catch (JAXRPCException e) {
       throw new RemoteException(e.getMessage(), e);
    } catch (Exception e) {
       if (e instanceof RuntimeException) {
          throw (RuntimeException)e;
       } else {
          throw new RemoteException(e.getMessage(), e);
       }
    }
 }

In the code above, we added the single line:

Serializer.attachPendingReportToMessage(_state.getMessageContext());

immediately before the _send() call. If there is no pending client report to send, attachPendingReportToMessage just returns. Otherwise, it serializes the current report to XML and adds it as a text attachment to the SOAP message. We make the same changes to the other business methods, adding the line above immediately before each of their _send() calls.

Message-driven bean

Now that we're recording client transaction response times and tunneling them through the HTTP/SOAP traffic, we need a way of processing these reports on the application server. A message-driven EJB component is appropriate for this task because response time reports should be handled asynchronously to the user transactions. The Web service Tie class queues the XML ClientReport objects. The EJB code below reads and processes these reports:

package TestBean;
import javax.jms.*;
import javax.ejb.*;
import Payload.*;
/**
 * Created May 23, 2003 6:01:04 PM
 * Code generated by the Sun ONE Studio EJB Builder
 * @author Brian
 */
public class TestMDBBean implements 
   javax.ejb.MessageDrivenBean, javax.jms.MessageListener {
   private transient javax.ejb.MessageDrivenContext context;
   
   
   
   /**
    * @see javax.ejb.MessageDrivenBean
              #setMessageDrivenContext
                  (javax.ejb.MessageDrivenContext)
    */
   public void setMessageDrivenContext
      (javax.ejb.MessageDrivenContext aContext) {
      context=aContext;
   }
   
   
   /**
    * See section 15.4.4 of the EJB 2.0 specification
    */
   public void ejbCreate() {
      
   }
   
   
   /**
    * @see javax.jms.MessageListener#onMessage(javax.jms.Message)
    */
   public void onMessage(javax.jms.Message aMessage) {
      TextMessage msg = null;
      try {
         if (aMessage instanceof TextMessage) {
            msg = (TextMessage) aMessage;
            String crXML = msg.getText();
            //System.out.println("MESSAGE BEAN: Message");
            //System.out.println(crXML);
            ClientReport cr = Serializer.ClientReportXML(crXML);
            System.out.println
            ("Took:" + String.valueOf(cr.getClientElapsedMS()) +
            " MS. for: " + cr.getType().toString());
         } else {
            System.out.println("Message of wrong type: " +
            aMessage.getClass().getName());
         }
      } catch (JMSException e) {
         System.err.println("MessageBean.onMessage: " +
         "JMSException: " + e.toString());
         context.setRollbackOnly();
      } catch (Throwable te) {
         System.err.println("MessageBean.onMessage: " +
         "Exception: " + te.toString());
      }
   }
   
   
   /**
    * @see javax.ejb.MessageDrivenBean#ejbRemove()
    */
   public void ejbRemove() {
      
   }
   
}

Normally, reports would be logged to a database for offline analysis and reporting. In our example, we just print the reports to the console to demonstrate that the client receives them. The onMessage() method uses Serializer.ClientReportXML() to create ClientReport objects from the queued XML strings. We want to do the decoding here to save processing time within the transaction processing workflow.

The sample implementation

All of the code needed to run the sample application is available for download from Resources. I developed the code using Sun ONE Studio, Enterprise Edition Update 1. This kit includes a J2EE development IDE as well as a J2EE application server, and its Web services utilities employ the embedded Java Web Service Developer Pack 1.0_01. Resources has information about the free download URL as well as links to developer documentation.

Build and deploy the server application

This section assumes you have a working knowledge of Sun ONE Application Framework. In addition, the instructions assume you use Microsoft Windows. The complete source code is already prebuilt for deployment. The Web service client and server packages are already generated (and modified for metrics-report serialization), but the description that follows also contains some background information on how the components were built within the Sun ONE IDE.

To build and deploy the server application, first, start an application server by selecting Programs from the Windows Start menu, then select Sun Microsystems, then Sun ONE Application Server, then Start Application Server.

Start the IDE using a similar Windows command. Click the Runtime tab in the Explorer window, check the installed servers, and ensure the application server's running instance is set as the default server.

Unzip the download kit to a directory and right-click on the Filesystems icon in the IDE Explorer window. Using the Mount/Local Directory dialog, issue two different mount commands:

  1. <download directory>/Metrics/Metrics opens the Payload package
  2. <download directory>/Metrics/TransactionServer opens the server EJB, Web module, and server application components

EJBModule_Xact is the application server prototype containing the three business methods: submitWork(), checkWork(), and getResult(). The TransactionProcessor/Xact_Module in the same directory is created by right-clicking the EJB and selecting New, then, JSP & Servlet, then the Web Module command. The Web module in the TransactionService package is generated by choosing New, then Web Services, then the Web Module wizard, and choosing the EJB component's Java methods as the source. The TransactionService/XactServiceGenServer is automatically generated by right-clicking in the Web Module and selecting Create New Web Service. But be careful before doing this, as it will wipe out the Serialize calls added to the Tie class.

The TransactionServerApp application is created by selecting New, then J2EE, then the Application command. Add components to the application by right-clicking on the empty application, selecting Add Modules, and selecting EJBModule_Xact and the Xact_Service Web Module.

This section gave a general idea of how the code components were built. You'll need to deploy these components to your application server. Inside the IDE, right-click on TransactionServerApp and deploy. Check the application server system console to ensure it deployed properly.

Build the sample client application

To build the sample client application, add the following filesystem to the IDE: <download directory>/Metrics/TransactionClient.

This filesystem consists of an application class that simulates execution of client transactions and the Xact package that contains the client Web service handlers.

The Xact package was built using the Sun Web Services Developer Pack, which is included with Sun ONE Application Framework. The batch file gen.bat issues the wscompile command to generate this package. If you want to rebuild this package, you may need to adjust the environment variable settings and the URL in the config.xml that it uses. However, if you do so, you'll overwrite the lines of code we added to the Stub class Web methods, so you'll need to replace them.

Let's look at XactClientApp, the sample client application class:

import Xact.*;
import javax.xml.rpc.Stub;
import Payload.*;
public class XactClientApp {
   
   /** Creates a new instance of XactClientApp */
   public XactClientApp() {
   }
   
   /**
    * @param args the command line arguments
    */
   
   public static void main(String[] args) {
      try {
         int cyclesPerXact = 1;
         int numberXacts = 5;
         String transactionID = "";
         String transactionType =
         String.valueOf(cyclesPerXact) +" submit,check,gets";
         Stub stub = createProxy();
         XactServiceServantInterface xact = (XactServiceServantInterface)stub;
         CurrentReport cr = new CurrentReport();
         for (int x=1; x<= numberXacts;x++){
            cr.BeginTransaction();
            for (int i=1; i<=cyclesPerXact;i++){
               transactionID = xact.submitWork("new transaction");
               System.out.println("Transaction:" + transactionID);
               boolean unused = xact.checkWork(transactionID);
               String ignore = xact.getResult(transactionID);
            }
            cr.CommitTransaction(transactionID, transactionType,"success");
         }
      } catch (Exception ex) {
         ex.printStackTrace();
      }
   }
   
   private static Stub createProxy() {
      return (Stub)(new XactService_Impl()).getXactServiceServantInterfacePort();
   }
}

Look at the inner loop. The client application determines what constitutes a business transaction. In our case, it's three Web service calls: one to submitWork(), one to checkWork(), and one to getResult(). The client delimits the transaction with beginTransaction() and commitTransaction(). During that loop's second iteration, a completed ClientReport will appear in the CurrentReport.LastReport object. When the client invokes submitWork(), the corresponding method in the Web service client Stub class invokes Serializer.attachPendingReportToMessage() to attach this report to the SOAP message.

cyclesperXact and numberXacts are used to control the number of Web service calls per transaction and the number of transactions the client submits in a run.

Right-click on the application icon XactClientApp; first select Build All, then Execute. In the execution window, you will see the application report the transaction identifiers it received for each transaction. Look in the application server Windows output console. You should see lines like the following:

INFO: CORE3274: successful server startup
INFO: CORE5053: Application onReady complete.
INFO: CORE3282: stdout: Exception occurred connecting to queue: javax.naming.Nam
eNotFoundException
INFO: CORE3282: stdout: Exception occurred connecting to queue: javax.naming.Nam
eNotFoundException
INFO: CORE3282: stdout: Exception occurred connecting to queue: javax.naming.Nam
eNotFoundException
INFO: CORE3282: stdout: Exception occurred connecting to queue: javax.naming.Nam
eNotFoundException

We haven't installed the application server metrics queue or deployed the application server metrics-reader EJB yet. The client generates the metrics attachment, the server receives it, and attempts to queue it to a nonexistent queue. The Serializer class just reports the error and allows the application to continue. Recall that one of our objectives is to maintain the overall reliability of the business transaction system. Here we see that even when the new metrics components fail, business-critical transactions still proceed normally.

Define the server metrics queue

To define the server metrics queue, we must create a Java Messaging Service (JMS) queue so that we can asynchronously pass metrics attachments from the Web service handlers to the metrics-handling EJB component.

The Sun ONE Application Server has an embedded Sun ONE Message Queue (MQ) server. You define a queue within the application server in three steps. First, define a physical destination. Then create an associated JMS destination resource. Finally, define a QueueConnectionPool to allow an application to connect to a queue and perform operations.

You complete all these tasks using the Application Server Administration Console. From Windows, select the Start menu, then Programs, then Sun Microsystems, then Sun ONE Application Server, then Start Administrator Console.

To create the physical destination, navigate to the application server instance, by choosing JMS, then Services, then Physical Destinations. Click the New button to create a new destination. Name the instance "TestMDBQueue," choose a queue type, and click OK.

Create the destination resource for the queue by navigating to the application server instance by selecting JMS, then Destination Resources. Click the New button, type "jms/TestMDBQueue" as the JNDI (Java Naming and Directory Interface) name, select the type as javax.jms.queue and click OK. After the destination is created, click on it, and then click its Properties button. Type in a single property, "imqDestinationName," with value TestMDBQueue. This associates the queue with the physical destination we created above.

Finally, we create the connection factory. Navigate to the application server instance by choosing JMS, and then Connection Factories; click New. Type "jms/TestMDBFactory" as the JNDI name, select the type as javax.jms.QueueConnectionFactory, and click OK.

You must apply the changes to instantiate the queue components. Accomplish that by navigating to the root application server instance and clicking on the Apply Changes button. You should see the new components created in the server console output window:

INFO: JMS5002: Binding [< JMS Destination: jms/TestMDBQueue, javax.jms.Queue, [
imqDestinationName=TestMDBQueue ] >]
INFO: JMS5002: Binding [< JMS Connection Factory: jms/TestMDBFactory, javax.jms.
QueueConnectionFactory, No properties >]

Build and deploy the server metrics reader EJB component

Now that we've created a server queue, we can deploy the message-driven EJB component that reads metrics attachments from it. First, mount the filesystem <download directory>/Metrics/MDBTester and click on the TestMDB(EJB) icon. In its property window, click the Sun ONE AS tab and examine the last two properties: the mapping references. Click on these properties and confirm that they reference the JMS resources we defined in the preceding step.

Right-click on EJBModule_TestMDB and select Deploy. After the EJB component deploys, we should see a message like the following in the application server console output window:

INFO: MDB00044: Deploying message-driven bean [EJBModule_TestMDB:TestMDB], consu
ming from [jms/TestMDBQueue]
INFO: MDB0001: Create message-driven bean pool with maximum pool size [640], bea
n idle timeout [600] seconds
INFO: MDB00022: [EJBModule_TestMDB:TestMDB]: Message-driven bean listening on JM
S destination [TestMDBQueue]
INFO: LDR5010: All ejb(s) of [EJBModule_TestMDB] loaded successfully!

This EJB component is kept in a separate module because we would normally want to deploy it on a different server instance than the transaction service application.

Run the sample implementation

Now that we've deployed all the metrics infrastructure, we can look at some client response-time reports. Execute XactClientApp again; this time, the following messages appear in the application server console output window:

INFO: CORE3282: stdout: Took:2613 MS. for: 1 submit,check,gets
INFO: CORE3282: stdout: Took:431 MS. for: 1 submit,check,gets
INFO: CORE3282: stdout: Took:4307 MS. for: 1 submit,check,gets
INFO: CORE3282: stdout: Took:871 MS. for: 1 submit,check,gets

Each time the metrics-reader EJB component receives a client report, it decodes it to a ClientReport and reports the clientElapsedMS field. In this example, both the IDE and the application server ran on a Compaq Presario laptop, so the reported response times are certainly not typical of true application server service times.

Note that while the client executed five transactions, there are only four reports. The last transaction from a client application session is never reported since there is no subsequent transaction for the completed report to ride along on.

This implementation is essentially a sampling. It gathers most transactions—certainly enough to measure service quality—but not all. Adjustments could be made to the client application to correct this. The Payload package could be enhanced to persist metrics reports across sessions, but that still wouldn't guarantee reports on all transactions. Recall that we still allow a business transaction to proceed if metrics gathering fails. Since that is a paramount objective, the overall system must always be viewed as a transactions sampler, not an exhaustive recorder.

Enhancements and further work

This implementation can be enhanced in several areas to provide even more benefit to the enterprise.

You can extend the ClientReport class so the client can report more information about the transaction. When analyzing response times at the server, the more detailed information we have about a transaction, the better. It's important to distinguish transactions that take a long time because they are inherently complex from transactions that take a long time because of server bottlenecks. Reduction in server bottlenecks is a principle objective. The best way to enhance ClientReport is to add an array of (attribute,value) strings that allow the client to report arbitrary characteristics of a transaction.

The metrics-gathering architecture can also help other aspects of enterprise service management. When a server has thousands or tens of thousands of clients in the field, it's important to ensure that server software upgrades are backwards compatible for the existing client base. But what software versions are clients running? This is often difficult to ascertain, but if the ClientReport class is extended with a client software version field, and also client operating system versions, a simple metrics database query would provide that information.

You can enhance the Payload package to report client software faults. Client exception handlers can save details about the context that caused a failure, and the Payload package can upload this information when the application restarts. This highlights one area of great potential: alarms. The metrics-reader EJB component can initiate a business workflow process when it receives a client software error report. It can raise an SNMP (Simple Network Management Protocol) trap or send an email, allowing customer service personnel to initiate a call to a user who has experienced a problem.

Receive accurate client response times

In this article, I showed how to layer transaction response-time recording onto an existing J2EE Web services application. This approach can be used to measure accurate response times from the perspective of a client application. The implementation is lightweight. No new network traffic is needed between the client and the server. Metrics payloads are queued for low-priority logging, so server resources can be reserved for application processing.

Brian Connolly is an author, software architect, and independent consultant. His specialty is enterprise transaction system design and performance analysis. He's lead development teams that have produced international trading systems for the foreign exchange and financial futures markets, data warehouses for the health care industry, and also a major media Website.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies