ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Using Jini to Build a Catastrophe-Resistant System, Part 2

by Dave Sag
05/15/2002

In the first part of this series, I discussed the technologies underlying an application that could resist the impact of a 767. I called this system Corporate Operating System, or COS. The need for such systems has forced the emergence of many new layers in application development, above and beyond the traditional n-tier structure. We have the Java Virtual Machine layer as a substrata, followed by a Service Container layer, in parallel with a basic set of ensemble provisioning services, such as those provided by Rio. Above that you now have a couple of application layers.

For some applications you may need to write specialized services. For this, you have access to all of the code of COS (described in detail in part one), and soon (hopefully) you'll also have access to the source code for Rio, as well as Jini. You could simply extend COS' AbstractCosJSB (a super-class of Rio's own Service Beans) and fill in the blanks, or for a more complex JSB that interacts with COS' serviceFlags, extend AbstractServiceFlagJSB. This extends AbstractCosJSB and like it, provides a method:

public abstract boolean performServiceOperation(CosEntryInterface entry,
    Transaction trans,
    String serviceparam)
  throws CriticalObjectStoreException, CosApplicationException;

This is the central method that executes a "worker" method on the target object. It takes two interesting parameters: the target entry object, and a string parameter that is used to select the method on the target to execute. The service parameter is often used to morph the behavior of this method, with each value relating to a different interface method on the primitive target entry.

The values of the service parameter are usually hard-coded into the definition of this method on the implementing service bean. They are also specified in an XML configuration file in the central COS system, and are distributed along with the ServiceConfiguration object. The parity of these two sources can be verified in the doServiceConfiguration method of the interface.

Each service extending this class follows the "Service Flag Pattern." This pattern connects the service to a named JavaSpace, where it listens for ServiceFlag with a property set to the name of that service bean implementation. ServiceFlag objects are messages describing the location of a target Entry in the space and a simple instruction as to what it is to do with that Entry.

Services have access to the full range of features of a Rio JSB, and as such can be good citizens of a J2EE or CORBA environment, or a JSP Web application.

COS Admin screen snapshot.
Figure 1. COS Admin Screen.

For many applications, the default services provided by COS -- JDBC archiving (Karen), HTTPPosting (Roger), garbage collection (Otto), distribution (Max), scheduling (Robin), emailing (Pat the Postman), and a growing list of others -- will work for you out of the box, so you can simply use a default ensemble configuration.

Figure 1 shows the COS admin tool having loaded the dawgTaskTracker.xml file that specifies the Ensemble application. This file is a copy of the default COS ensemble file, the only difference being the line <CosApplication name="dawgTaskTracker" apptype="Dawg">.

A COS client application is simply an application that connects to a named COS ensemble. The client application may simply extend AbstractClientApp and call on its inherited connect() method to perform the actual connection. For security reasons, the COS admin tool is to be used to deploy, start, and stop applications. Your client application will simply connect to the named application if it is running.

Once connected to the COS, your application provides a handy CosConnectable object with create, retrieve, update, delete, and other methods you can use to manage shared information.

Our sample application is an example of what we call a distributed, ad hoc workgroup ("Dawg") application, called TaskTracker. (Great name, huh?)

TaskTracker works as follows: Users log in and report a task or tasks. They allocate the tasks to their buddies. The task itself is represented by an instance of a TaskTrackerEntry that is passed between the COS' JavaSpace and your GUI client app.

Let's take a quick look at the actual TaskTrackerEntry:

public class TaskTrackerEntry extends CosEntry implements Archivable

Task List Screen Snapshot.
Figure 2. Task List.

A CosEntry is a simple extension to the JavaSpace's Entry class, which serves as a COS-specific marker. CosEntries add an application context and a creationEnsembleName, as well as creation and modification dates. This associates the CosEntry with an ensemble. When cos.create(entry) is called, the entry's setCreationEnsembleName() is called. Like JavaSpace entries, all CosEntries must have null constructors and public properties.

In addition to a null constructor, our TaskTrackerEntry provides a more useful constructor for a client application to use when actually creating a new TaskTrackerEntry.

public TaskTrackerEntry (String projectname,
    String name,
    Integer priority,
    String category,
    String blurb,
    String message,
    String user)

The basic principles of the TaskTracker are as follows:

This inverts the traditional ERP (Enterprise Resource Planning) or MS ProjectPlan-style project management system in that, although you do specify tasks in a similar way and can easily add sophisticated task dependencies and so forth, only the description of the tasks is done from the top down, and then only at first. The Dawg model encourages a bottom-up approach to task and resource allocation. It permits people to broadcast and refine what they want and to volunteer for duty.

Now, you could build such a system on top of any peer-to-peer system, and I'm sure, now that the idea is out there, someone will build a really great one. The main point of this example is to show you how simple it can be to build an app takes advantage of the inherent 911-proof nature of the COS system.

Within our TaskTrackerEntry we define a bunch of statuses:

    /** the task has not yet been assigned to anyone. **/
    public static final int STATUS_OPEN = 0;

    /** the task has been assigned to someone. use {@link #getCurrentOwner}
     *  to see who it has been assigned to. **/
    public static final int STATUS_ASSIGNED = 1;

    /** a person has accepted the task. **/
    public static final int STATUS_ACCEPTED = 2;

    /** a person thinks they have fixed their task. **/
    public static final int STATUS_COMPLETED = 3;

    /** a tester has tested the fixed task and the test passed. **/
    public static final int STATUS_TESTED = 4;

    /** the task is done with. **/
    public static final int STATUS_DONE = 5;

The rest of the Entry is taken up with properties such as

    public String mProjectName;
    public Integer mTaskNumber;
    public Long mReplaces;
    public Vector mDependencies;
    public Vector mSeeAlso;
    public String mName;
    public Integer mPriority;
    public Integer mUrgency;
    public String mCategory;
    public String mSubCategory;
    public Integer mStatus;
    public String mCurrentOwner;
    public String mManager;
    public Vector mPastOwners;
    public String mBlurb;
    public String mTestDescription;
    public Vector mMessages; 
    public Date mFinalised;
    public Date mEtc;

For completeness, the entry includes any associated getters and setters, and a few utility methods such as:

    public void done() {
        // the task has been COMPLETED and TESTED
        mStatus = new Integer(STATUS_DONE);
        mFinalised = new Date();
    }

    public void accepted(Date etc) {
        mStatus = new Integer(STATUS_ACCEPTED);
        mEtc = etc;
    }

and

    public void assignToOwner(String owner) {
    
        if (owner == null) {
            return; 
        }
    
        if (mCurrentOwner.equals(owner)) {
            return;
        }

        if (mCurrentOwner != null) {
            if (mPastOwners == null) {
                mPastOwners = new Vector();
            }
            mPastOwners.add(mCurrentOwner);
        }
        mCurrentOwner = owner;
        if (mCurrentOwner.equals(EMPTY_STRING)) {
            mStatus = new Integer(STATUS_OPEN);
        }
    }

In order to satisfy the requirements of the Archivable interface we must fill in the following methods:

    public boolean archive(SpaceConnectable spconn, Connection jdbcconn)
      throws CriticalObjectStoreException;
      
    public boolean extract(SpaceConnectable spconn, Connection jdbcconn)
      throws CriticalObjectStoreException;

Karen, the COS implementation of the Archivist service, has two service modes, archive and extract. When archive is called, the service is archiving the entry to a JDBC database at jdbcconn. Your entry should provide the specific detail needed to perform this archiving. extract is called when Karen is extracting the entry from a JDBC database and inserting it as a fully formed entry into the JavaSpace. In the case of the TaskTracker, for now we will just return true from these methods until we are ready to fill them in.

Quite a bit of the universal information in our TaskTracker is contained in a lot of small lists, so we will use a ListEntry to help us out a bit.

public class ListEntry extends CosEntry implements Archivable

Each list entry has two public properties:

    /** the name of this list */
    public String mListname;

    /** the list. **/
    public Vector mList;

As vectors get larger, they get harder to serialize and transport. With large lists, the cost of the RMI transactions makes them impractical. In this case, one can use more comprehensive techniques to message between the client, the COS, and any underlying database that stores the data. Messages in the form of SQL statements can be passed instead of the objects themselves.

The list includes an equals method to compare two lists, and methods to add items, remove items, and so forth. The ListEntry is, in reality, a named shared vector and is ideal for small lists.

From this basic ListEntry we make a UserListEntry for short lists of the user's buddies, tasks they are working on, and tasks they've reported.

The client consists of a main application ClientApp that extends COS' AbstractClientApp and a bunch of Swing GUI JFrames for login, task lists, task editing, and so forth. Our client is 99% user interface.

The main ClientApp uses property change events to notify the various JFrames of changes, and registers a CosEventListener from the COS for changes to the global shared list of projects, the users's buddies, tasks, and so forth.

As an example, it is useful to work through what happens when a new task is created in the GUI. Every frame, when created, is passed the ClientApp, providing a local ApplicationContext. The New Task button's action listener creates a new TaskTrackerEntry locally and passes it to ClientApp.newTask(task). newTask adds a TaskTrackerEntry to the COS.

This involves the following train of logic:

  1. Create a transaction to wrap the entire creation of the task.
  2. Create the task, thus returning the task's universally unique ID.
  3. Examine the project name that came with this task.
    1. Does it exist in our global list of projects? Does the global list of projects exist? If it doesn't, we had best make one and add it to the system.
    2. If the supplied project doesn't exist in the current system, add it to the global project list.
  4. Examine the category that came with this task.
    1. Does it exist in our global list of tasks? Does the global list of tasks exist?
    2. If it doesn't, we had best make one and add it to the system.
    3. If the supplied category doesn't exist in the current system, add it to the global category list.
  5. Examine the user assigned to the task. Do we know him or her? Do we even have a buddy list? If we don't, then create it.
  6. If the user is not already in your buddy list, then add it.
  7. Get that user's list of tasks and add this task.
  8. If the list doesn't exist, you must create it. This way when the user first logs in he or she can have a task already assigned.
  9. Then, once again, for the list of tasks the user is working on.
  10. Commit the transaction if everything went smoothly. Abort if not.

In order to simplify our code we have written a small private utility method called addItemToGlobalList. Happily, this method provides an excellent example of the patterns used for writing COS code.


Example 1. addItemToGlobalList

/**
 *      addItemToGlobalList first checks to see if the named list exists.
 *      if it does exist then we check to see if the String item is contained 
 *      by the list.
 *      if it doesn't exist we build a list of that name, and add the item to it,
 *      then cos.create it.
 *      @param item the String we are seeking to add to the list.
 *      @param listname the name of the list
 *      @param tran the transaction covering this operation.
 *      @return true if all went okay, false if not. 
 *          you should abort the transaction if the result of this operation is false.
 */
private boolean addItemToGlobalList(String item,String listname, Transaction tran) {
    ListEntry template = new ListEntry();
    template.mListname = listname;
    
    ListEntry known;
    
    try {
        known = (ListEntry)mCosConnector.retrieve(template,tran);
    } catch (GeneralCosException e) {
        Messenger.logErr("GeneralCosException: Can't retrieve the list \""+listname+"\".
          "+e.getMessage());
        return false;
    }

    if (known == null) {
            // so create a default list and pop it into the cos.
        known = (ListEntry)template.clone();
        known.add(item);
        try {
            SpaceID kcid = mCosConnector.create(known,tran,getApplicationName());
            Messenger.debug("created a list of projects with only 
                one project, \""+item+"\".");
        } catch (GeneralCosException e) {
            Messenger.logErr("GeneralCosException: Can't create a default list of known 
              projects. "+e.getMessage());
            return false;
        }
    }
    
    Vector p = known.getList();
    
    try{
        if (p == null) {
            Messenger.logErr("Strange behaviour:  The List exists 
               but is empty.  It should always have 1 item in it at least.");
            known.add(item);
            mCosConnector.update(known,tran);
        } else {
            if (!(p.contains(item))) {
                known.add(item);
                mCosConnector.update(known,tran);
            }
        }
    } catch (GeneralCosException e) {
        Messenger.logErr("GeneralCosException: Can't update the 
          list of known categories. "+e.getMessage());
        return false;
    }
    
    return true;
}

The pattern is as simple as the general pattern for any Jini application; specify your template and perform an associative match to get the group of things you wish to examine in more detail.

By wrapping eveything in a transaction, if any part of the process fails, the whole thing fails.

By either registering your ClientApp as a CosEventListener, or by taking advantage of Rio's Watchable interface, you can react to changes in watched data within the system. So, for example, when you add a category to the global list of categories, every client connected to the COS gets the updated category list via a remote event. These remote events cause changes to the client's local cache of categories, thus triggering a PropertyChangeEvent that is heard by the local GUI elements.

Similarly, when you assign a task to one of your buddies, her task list will be updated by your client. That change will be transmitted to her client if she is connected to the COS. As these are RMI remote events, they cannot be relied upon to always succeed and are not used for mission-critical events; however, the damage done to a GUI by missing the occasional update here and there is not going to cause anyone to lose sleep. The client will get another event shortly, or will repoll the COS if it hasn't heard anything in a while. Either way, the changes get through eventually.

Ensemble systems are built upon the notion that all of their component parts will probably fail unexpectedly; this includes events. Your application is more than the sum of its parts, however; what is unreliable at the component level is seamless and reliable at the application level. When people criticize RMI events for their unreliability, they are making a classic "level error."

Jini-based ensemble applications are only really secure and simple on your local network right now, but Rio's JSP interface and Lincoln Tunnel, as well as Crudlet-style XML interfaces to COS, allow thin clients, Web pages, Flash movies, and so forth to connect to a COS system as if it were any other web service.

As Jini's Davis project (login required) matures, permitting secure, authenticated, ad-hoc, distributed networks, the patterns developed within the LAN become scalable to the global network.

You can download the TaskTracker Javadocs and a regular developer build of the TaskTracker from www.davesag.org. The build includes full time-stamped source code, from which the above examples are taken, as well as JavaDocs and an Ant build script. You will not be able to build it without COS, however, as it relies on several COS-specific APIs.

The COS Javadocs are online at cosproject.sourceforge.net. The COS source code is being prepared for release soon. Current plans are to separate out the API code from the implemetation code, much in the manner of Jini, before the source is released.

Resources

Rio

Using Jini to Build a Catastrophe-Resistant System, Part 1

Dave Sag is a skilled Object Modeler and Software Designer, using Java as well as other programming environments.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.