Transaction Example

Class Overview
TxnGuide.java
InfoKeeper.java
XMLWriter.java

The following code provides a fully functional example of a multi-threaded transactional BDB XML application. The example creates multiple threads, each of which creates a set number of XML documents that it then writes to the container. Each thread creates and writes 10 documents under a single transaction before committing and writing another 10 documents. This activity is repeated 50 times.

From the command line, you can tell the program to vary:

As we will see in Runtime Analysis each of these variables plays a role in the number of deadlocks the program encounters during its run time.

Of course, each writer thread performs deadlock detection as described in this manual. In addition, normal recovery is performed when the environment is opened.

Class Overview

This example program uses three classes:

  • TxnGuide.java

    This is our main program. It opens and closes our environment, manager, and container and it spawns our worker threads for us.

    This class is described in TxnGuide.java.

  • InfoKeeper.java

    This is a utility class that we use to pass information to, and retrieve information from, our worker threads.

    This class is described in InfoKeeper.java.

  • XMLWriter.java

    This is our thread implementation. It performs all of our container writes for us. This class is where all of our transaction commit/abort code resides. We also perform deadlock handling in this class.

    This class is described in XMLWriter.java.

TxnGuide.java

This is our "main" class. We will use this class to open our environment, manager and container, and to spawn our worker threads.

To begin, we provide the normal package statement and the necessary import statements.

// File TxnGuide.java

package dbxml.txn;

import com.sleepycat.dbxml.XmlContainer;
import com.sleepycat.dbxml.XmlContainerConfig;
import com.sleepycat.dbxml.XmlException;
import com.sleepycat.dbxml.XmlManager;
import com.sleepycat.dbxml.XmlManagerConfig;

import com.sleepycat.db.DatabaseException;
import com.sleepycat.db.Environment;
import com.sleepycat.db.EnvironmentConfig;
import com.sleepycat.db.LockDetectMode;

import java.io.File;
import java.io.FileNotFoundException;  

Next we declare our class and we define the private data members that our class needs.

public class TxnGuide {

    private static String myEnvPath = "./";
    private static String containerName = "txn.dbxml";

    // DBXML handles
    private static Environment myEnv = null;
    private static XmlManager mgr = null;
    private static XmlContainer container = null;

    private static InfoKeeper ik = null; 

Now we implement our usage() method. The information presented here is a duplicate of what we have already described about this application in the preceding text. In addition, we also describe the exact command line switches that this application supports.

   private static void usage() {
        String msg =  "\nThis program writes XML documents to a DB XML";
               msg += "container. The documents are written using any\n";
               msg += "number of threads that will perform writes\n";
               msg += "using 50 transactions. Each transaction writes \n";
               msg += "10 documents. You can choose to perform the ";
               msg += "writes using default isolation, or using \n";
               msg += "READ COMMITTED isolation. If READ COMMITTED ";
               msg += "is used, the application will see fewer";
               msg += "deadlocks.\n\n";

               msg += "Note that you can vary the size of the documents ";
               msg += "written to the container by defining the number\n";
               msg += "of nodes in the documents. Up to a point, and";
               msg += "depending on your system's performance,\n";
               msg += "increasing the number of nodes will increase\n";
               msg += "the number of deadlocks that your application\n";
               msg += "will see.\n\n";

               msg += "Command line options are: \n";
               msg += " -h <database_home_directory>\n";
               msg += " [-t <number of threads>]\n";
               msg += " [-n <number of nodes per document>]\n";
               msg += " [-w]       (create a Wholedoc container)\n";
               msg += " [-2]       (use READ COMMITTED isolation)\n";

        System.out.println(msg);
        System.exit(-1);
    }  

Now for our main() method. To begin, we instantiate an ParseArgs object (see InfoKeeper.java for information on this object), and then we parse our command line arguments. As a part of parsing those arguments, we will fill our InfoKeeper object with relevant information obtained from the command line. Once our argument list is parsed, we open our environment, manager and container.

We parse our argument list and open our environment using methods private to this class. We describe these methods in a moment.

    public static void main(String args[]) {
        try {
            ik = new InfoKeeper(5,       // Num Threads
                                1,       // Num Nodes
                                true,    // Node storage container?
                                false);  // Use ReadCommitted Isolation?
            // Parse the arguments list
            parseArgs(args);
            // Open the environment and databases
            openEnv();  

After that, we need to spawn and join our worker threads. The XMLWriter class that we use here is described in XMLWriter.java.

            // Start the threads
            XMLWriter[] threadArray;
            threadArray = new XMLWriter[ik.getNumThreads()];
            for (int i = 0; i < ik.getNumThreads(); i++) {
                threadArray[i] = new XMLWriter(mgr, container, ik);
                threadArray[i].start();
            }

            for (int i = 0; i < ik.getNumThreads(); i++) {
                threadArray[i].join();
            }  

Once the threads have all completed, we print some statistics gathered during the program run.

            // Report the run's results
            System.out.println("\n\n\n");
            System.out.println("Number of threads:\t\t"
                + ik.getNumThreads());
            System.out.println("Number of doc nodes:\t\t"
                + ik.getNumNodes());
            System.out.println("Using node storage:\t\t"
                + ik.getIsNodeStorage());
            System.out.println("Using read committed:\t\t"
                + ik.getReadCommitted());
            System.out.println("\nNumber deadlocks seen:\t\t"
                + ik.getNumDeadlocks());  

Finally, we catch and manage all our exceptions. Notice that we call another private member, closeEnv(), in order to close our container and manager. We describe this method next.

        } catch (XmlException xe) {
            System.err.println("TxnGuide: " + xe.toString());
            xe.printStackTrace();
        } catch (DatabaseException de) {
            System.err.println("TxnGuide: " + de.toString());
            de.printStackTrace();
        } catch (Exception e) {
            System.err.println("TxnGuide: " + e.toString());
            e.printStackTrace();
        } finally {
            closeEnv();
        }
        System.out.println("All done.");
    }  

Having completed our main() method, we now implement our private closeEnv() method. The only thing to notice here is that we close the container and manager, but not the environment. As we will see in the next method, we allow our manager to adopt the environment. This means that when the manager is closed, then so is the environment.

    private static void closeEnv() {
        if (container != null) {
            try {
                container.close();
            } catch (XmlException xe) {
                System.err.println("closeEnv: container: " +
                    xe.toString());
                xe.printStackTrace();
            }
         }

        if (mgr != null) {
            try {
                mgr.close();
            } catch (XmlException xe) {
                System.err.println("closeEnv: mgr: " +
                    xe.toString());
                xe.printStackTrace();
            }
        }
    }  

Now we can implement our openEnv() method. As always, we begin by setting up our EnvironmentConfig object. Here, we configure the environment for transactional processing. We also allow the environment to be created if it does not already exist and we cause normal recovery to be run when the environment is opened.

    private static void openEnv() throws DatabaseException {
        System.out.println("opening env");

        // Set up the environment.
        EnvironmentConfig myEnvConfig = new EnvironmentConfig();
        myEnvConfig.setAllowCreate(true);
        myEnvConfig.setInitializeCache(true);
        myEnvConfig.setInitializeLocking(true);
        myEnvConfig.setInitializeLogging(true);
        myEnvConfig.setRunRecovery(true);
        myEnvConfig.setTransactional(true);
        // EnvironmentConfig.setThreaded(true) is the default behavior 
        // in Java, so we do not have to do anything to cause the
        // environment handle to be free-threaded.   

We want to perform deadlock detection, so we configure that next. Here, we choose to resolve deadlocks by picking the thread with the smallest number of write locks. The thread with the smallest number of write locks is the one that has performed the least amount of work. By choosing this thread for the abort/retry cycle, we minimize the amount of rework our application must perform due to a deadlock.

        myEnvConfig.setLockDetectMode(LockDetectMode.MINWRITE);  

Now we simply open the environment.

        try {
            // Open the environment
            myEnv = new Environment(new File(myEnvPath),    // Env home
                                    myEnvConfig);

        } catch (FileNotFoundException fnfe) {
            System.err.println("openEnv: " + fnfe.toString());
            System.exit(-1);
        }  

Now we open the manager. Notice that when we configure the manager, we cause it to adopt the environment. As we stated above, this causes the manager to close our environment when it closes. We also remove our container if it happens to already exist; this program only ever begins with new, empty containers as a result.

        try {
            XmlManagerConfig managerConfig = new XmlManagerConfig();
            // Close the environment when the manager closes
            managerConfig.setAdoptEnvironment(true);
            mgr = new XmlManager(myEnv, managerConfig);

            // If the container already exists, delete it. We don't want
            // naming conflicts if this program is run multiple times.
            if (mgr.existsContainer(containerName) != 0) {
                    mgr.removeContainer(containerName);
            }  

Finally, we open our container. Notice how we define the container's node storage type based on the information recorded in our InfoKeeper object. This information will be set based on data collected from the command line when we parse our argument list.

            // Open the container
            XmlContainerConfig containerConf = new XmlContainerConfig();
            containerConf.setTransactional(true);
            containerConf.setAllowCreate(true);
            // Declare the container type; that is, whether it is a 
            // node-storage or a whole doc container. If -w is specified
            // at the command line, the container is set to wholedoc,
            // otherwise node-storage is used.
            containerConf.setNodeContainer(ik.getIsNodeStorage());
            container = mgr.openContainer(containerName, containerConf);
        } catch (XmlException xe) {
            System.err.println("TxnGuide: " + xe.toString());
            xe.printStackTrace();
        }
    }  

As a final bit of work, we implement our parseArgs() method. Notice that we save most of the information collected here to our InfoKeeper object for later usage and reporting.

    private static void parseArgs(String args[]) {
        for(int i = 0; i < args.length; ++i) {
            if (args[i].startsWith("-")) {
                switch(args[i].charAt(1)) {
                    case 'h':
                        myEnvPath = new String(args[++i]);
                        break;
                    case 't':
                        ik.setNumThreads(Integer.parseInt(args[++i]));
                        break;
                    case 'n':
                        ik.setNumNodes(Integer.parseInt(args[++i]));
                        break;
                    case 'w':
                        ik.setIsNodeStorage(false);
                        break;
                    case '2':
                        ik.setReadCommit(true);
                        break;
                    default:
                        usage();
                }
            }
        }
    }
}  

InfoKeeper.java

InfoKeeper is a simple utility class used to maintain information of interest to the various threads in our example program. The information that it contains is either needed by the various threads of control in order to know how to run, or is useful for reporting purposes at the end of the program run, or both.

InfoKeeper is a trivial class that we present here only for the purpose of completeness.

// File InfoKeeper.java
package dbxml.txn;

public class InfoKeeper
{
    private static int numThreads;           // Number of threads to use

    private static int numNodes;             // Number of nodes per
                                             // document to generate.

    private static boolean doNodeStorage;    // Use node storage?

    private static boolean doReadCommitted;  // Use read committed 
                                             // isolation?

    private static int deadlockCounter;      // Number of deadlocks seen
                                             // in this program run.

    InfoKeeper(int nThreads, int nNodes, boolean nStorage, 
               boolean rCommit)
    {
        numThreads = nThreads;
        numNodes = nNodes;
        doNodeStorage = nStorage;
        doReadCommitted = rCommit;
        deadlockCounter = 0;
    }

    public synchronized void setNumThreads(int n) {
        numThreads = n;
    }

    public synchronized void setNumNodes(int n) {
        numNodes = n;
    }

    public synchronized void setIsNodeStorage(boolean n) {
        doNodeStorage = n;
    }

    public synchronized void setReadCommit(boolean n) {
        doReadCommitted = n;
    }

    public synchronized void incrementDeadlockCounter() {
        deadlockCounter++;
    }

    public synchronized int getNumThreads() {
        return numThreads;
    }

    public synchronized int getNumNodes() {
        return numNodes;
    }

    public synchronized boolean getIsNodeStorage() {
        return doNodeStorage;
    }

    public synchronized boolean getReadCommitted() {
        return doReadCommitted;
    }

    public synchronized int getNumDeadlocks() {
        return deadlockCounter;
    }
} 

XMLWriter.java

XMLWriter is our thread implementation. It is used to create a series of XML documents and then write them to the container under the protection of a transaction. The documents this class generates will vary in size depending on the number of nodes specified at the command line. Beyond that, however, each thread always performs 50 transactions and a total of 10 documents are written to the container for each transaction.

Each program run can also be varied somewhat in terms of the isolation level used by the transactions. Normally default isolation is used, but the user can relax this at the command line to use read committed isolation instead. Doing so can have positive implications for the program's throughput. See Runtime Analysis for more information.

To begin, we import our necessary classes. Note that java.util.Random is used to generate random data to place into our XML documents.

// File XmlWriter.java
package dbxml.txn;

import com.sleepycat.dbxml.XmlContainer;
import com.sleepycat.dbxml.XmlException;
import com.sleepycat.dbxml.XmlManager;
import com.sleepycat.dbxml.XmlTransaction;
import com.sleepycat.dbxml.XmlUpdateContext;

import com.sleepycat.db.TransactionConfig;

import java.util.Random; 

Then we declare our class and we initialize several private data members necessary to our class' operation. Notice the MAX_RETRY variable. This is used for our deadlock retry loop; our threads will never retry a transaction that deadlocks more than MAX_RETRY times.

public class XMLWriter extends Thread
{
    private Random generator = new Random();
    private boolean passTxn = false;

    private XmlManager myMgr = null;
    private XmlContainer myContainer = null;
    private InfoKeeper ik = null;

    private static final int MAX_RETRY = 20; 

Next we implement our class constructor. We set our BDB XML handles here. We also get our InfoKeeper object here, which is used to determine some important aspects of how our thread is supposed to behave.

    // Constructor. Get our DBXML handles from here
    XMLWriter(XmlManager mgr, XmlContainer container, InfoKeeper info)
        throws XmlException {
        myMgr = mgr;
        myContainer = container;
        ik = info;
    } 

Next we implement our run() method. Every java.lang.Thread class must implement this method. This method is where our class will perform actual work, so this is where we perform our transactional writes. This is also where we will perform all our deadlock handling.

This method begins by defining a few variables of interest to us. Notice that this is where we discover how many nodes we should generate per XML document and whether read committed isolation should be used by our transactions.

    public void run () {
        Random generator = new Random();
        XmlTransaction txn = null;
        int numNodes = ik.getNumNodes();
        boolean useRC = ik.getReadCommitted(); 

Then we begin our main transaction loop. As we do so, we initialize our retry variable to true and our retry_count variable to 0. So long as retry_count does not exceed MAX_RETRY, we will retry deadlocked transactions.

        // Perform 50 transactions
        for (int i=0; i<50; i++) {
           boolean retry = true;
           int retry_count = 0; 

Now we enter our retry loop, and immediately the try block that we use to perform deadlock handling and transaction aborts (if required). At the top of the loop, we obtain an update context and a TransactionConfig object. We use the TransactionConfig object to identify the whether we should use read committed isolation. Of course, this is driven ultimately by information passed to the program from the command line.

           // while loop is used for deadlock retries
           while (retry) {
                // try block used for deadlock detection and
                // general exception handling
                try {
                    XmlUpdateContext context = myMgr.createUpdateContext();
                    // Configure whether the transaction will use Read 
                    // Committed isolation. If -2 is specified on the 
                    // command line, then Read Committed is used.
                    TransactionConfig tc = new TransactionConfig();
                    tc.setReadCommitted(useRC); 

Now we (finally!) create our documents and write them to the container. Each document is written under the protection of a transaction. Each transaction is committed once 10 documents have been created and written.

                    // Get a transaction
                    txn = myMgr.createTransaction(null, tc);

                    // Write 10 records to the container
                    // for each transaction
                    for (int j = 0; j < 10; j++) {
                        // Get a document ID
                        String docID = getName() + i + j;

                        // Build the document
                        String theDoc = "<testDoc>\n";
                        for (int k = 0; k < numNodes; k++) {
                            theDoc += "<payload>" +
                                generator.nextDouble() + "</payload>\n";
                        }
                        theDoc += "</testDoc>";

                        // Put the document
                        myContainer.putDocument(txn,
                                          docID,
                                          theDoc,
                                          context);

                    } // end inner for loop

                    // Commit
                    txn.commit();
                    txn = null;
                    retry = false; 

To wrap things up, we have to perform error handling. First we look at XmlException objects. In particular, we need to look for deadlocks by examining the object to see if it encapsulates a com.sleepycat.db.DeadlockException. If it does, we will abort (this occurs in the finally clause, and then retry if our retry count is low enough.

                } catch (XmlException xe) {
                    retry = false;

                    // First, look for a deadlock and handle it
                    // if that is the cause of the exception.
                    if (xe.getDatabaseException() instanceof
                            com.sleepycat.db.DeadlockException) {

                        System.out.println(getName() +
                            " got deadlock exception!");
                        ik.incrementDeadlockCounter();

                        // retry if necessary
                        if (retry_count < MAX_RETRY) {
                            System.err.println(getName() +
                                " : Retrying operation.");
                            retry = true;
                            retry_count++;
                        } else {
                            System.err.println(getName() +
                                " : out of retries. Giving up.");
                        }
                    } else {
                            System.err.println("Error on txn commit: " +
                                xe.toString());
                    } 

For general all-purpose exceptions, we simply refrain from retrying the operation.

                } catch (Exception e) {
                    System.err.println(getName() +
                        " got general exception : " + e.toString());
                        retry = false; 

Finally, we always abort the transaction if its handle is non-null. The thread will then retry the transaction if the value of the retry allows it.

                } finally {
                    if (txn != null) {
                        try {
                            txn.abort();
                        } catch (Exception e) {
                            System.err.println("Error aborting txn: " +
                                e.toString());
                            e.printStackTrace();
                        }
                    }
                }

            } // end retry loop
        } // end for 50 transactions loop
    } // end of the run method
} // end of class 

This completes our transactional example. If you would like to experiment with this code, you can find the example in the following location in your BDB XML distribution:

BDBXML_INSTALL/dbxml/examples/java/txn

In addition, please see Runtime Analysis for an analysis on the performance characteristic illustrated by this program.