Make room for JavaSpaces, Part 4

Explore Jini transactions with JavaSpaces

The Jini transaction model is one of the lesser known and least used aspects of Jini, yet it provides a powerful tool for writing distributed applications that operate correctly in the presence of partial failure. In this article, we take a look at the Jini transaction model and show how you can use it with the JavaSpaces service -- one of the first Jini services to fully support Jini transactions.

TEXTBOX: TEXTBOX_HEAD: Make room for JavaSpaces: Read the whole series!

:END_TEXTBOX

In general, transactional systems let you group together a set of operations so that they are performed atomically -- that is, either all of the operations complete, or none of them do. Without this transactional ability, the states of systems can easily become inconsistent -- especially distributed systems in which participants can crash the network or leave the network before an operation has completed. By using transactions, you can ensure that the operations do complete, or if they don't, that the state of the entire distributed system remains unchanged.

Systems that support transactions, such as a database management system, typically build transactions into the system's core. Jini takes a more lightweight and flexible approach: It provides a transaction service that manages a set of participants through a transaction process. The transaction service leads the participants through a "two-phase commit protocol," a fairly simple and standard protocol that ensures that either all participants complete their respective operations in the transaction, or none of them do. If you're interested, you can find out more about this protocol in the Jini Transaction Specification (see Resources below).

The participants in a Jini transaction are typically Jini services and devices. If you are developing a Jini service, you can enable it to participate in Jini transactions by implementing the TransactionParticipant interface. You can find out more about this interface in the Jini Transaction Specification.

Now we'll give you a better idea of how you can use transactions with JavaSpaces.

Transactions and JavaSpaces

The JavaSpaces application programming interface integrates Jini transactions in a clean and well-thought-out manner. As a result, introducing transactional security into your JavaSpaces applications is usually fairly painless. To use transactions with space-based operations, typically you first ask a transaction manager to create a transaction and manage it for a specified lease time. Then, you pass the transaction to each space operation you'd like to occur under the transaction (which may include operations over more than one space). Assuming there are no problems along the way, you then explicitly commit the transaction, which results in all operations completing. If any problems occur, you can abort the transaction, which will leave the space unchanged. The transaction might also be aborted by the transaction manager if, for instance, the transaction's lease expires.

When you write an entry into a space under a transaction, the entry is only seen "within" the transaction until it commits. This means that the entry is invisible to any client attempting to read, take, or notify it outside of the transaction. If the entry is taken within the transaction, it will never be seen outside of the transaction. If the transaction aborts, the entry is discarded. Once the transaction commits, the entry is available for reads, takes, and notifications outside of the transaction. For more details, let's look at each space operation and how it operates under a transaction.

Let's start with the write operation, which takes an entry, a transaction, and a lease:

space.write(Entry entry, Transaction txn, Lease lease);

The operation writes the lease into the space, under the given transaction, and requests the specified lease time for the entry.

You might recall that, up to now in the JavaSpaces series, we've always used a null transaction as the second parameter to write, which assumes the operation consists of one indivisible action (the operation itself). As soon as the operation completes, the entry is visible to all clients of the space. On the other hand, when we write an entry under a non-null transaction, the entry is not accessible to operations outside of the transaction until the transaction commits. If the transaction commits, then all the entries written under the transaction become visible to the entire space. However, if the transaction aborts, the entries written under the transaction are removed. In effect, after the transaction aborts, the space reflects that the operations never occurred.

Now let's look at take and read. You will recall that both take and read take a template and return a matching entry from the space, if one exists. The take operation removes the entry before returning it, while the read operation returns a copy of the entry. When you take or read entries from the space under a transaction, they can come from entries written under or outside the transaction. If the transaction aborts, any entries taken under the transaction are returned to the space (and of course, any entries written under it are removed), leaving the space as if the operations never occurred.

Finally, you can also use notify under a transaction. When you register for a notify under a transaction, you receive notifications of entries that are written within the transaction and to the general space. When the transaction completes (whether it commits or aborts), all notification registrations under the transaction are withdrawn. If the transaction commits, the entries remaining in the transaction may result in notifications as response to registrations in the general space. The entries also become eligible for read and take operations from the space.

Now that you understand the semantics of using transactions with the space operations, let's move on to creating transactions via the transaction manager, and then write code that makes use of transactions and spaces.

Using a transaction manager

To make use of transactions, you first need access to a transaction manager that can create and maintain your transactions for you. To locate a manager, you use Jini's lookup and discovery. Like all Jini services, the lookup service returns a proxy object to a transaction manager; in this specific case, you'll be looking for a service that implements the TransactionManager interface. You might want to refer to Bill Venners's previous column on lookup and discovery (see Resources) for the specifics of locating a Jini service. Here you are going to use a simple utility class from our book JavaSpaces Principles, Patterns and Practice that obtains a handle to a transaction manager proxy (please refer to the book for the details of this utility class). Here is the code you use to obtain a proxy using the utility class:

TransactionManager mgr = TransactionManagerAccessor.getManager();

Here you call the getManager static method of the TransactionManagerAccessor class, which returns a TransactionManager proxy object. With this proxy in hand, you can create a transaction that will manage a set of operations over one or more Jini services (such as a JavaSpace), which implement the TransactionParticipant interface.

Now we'll show you how to create the transaction:

Transaction.Created trc = null;
try {
 trc = TransactionFactory.create(mgr, 300000);
} catch (Exception e) {
 System.err.println("Could not create transaction " + e);
}

First you declare a variable trc of type Transaction.Created (an inner class of Transaction), which is the type of object that will be returned when you ask the transaction manager to create a new transaction (we'll return to the inner class shortly, since the syntax may be a bit confusing). To create a transaction, you then use the TransactionFactory class and call its static method create, which takes a transaction manager and a lease time (in milliseconds) as parameters and creates a transaction that the supplied manager manages for the given lease time (in this code, you should request a lease of 5 minutes). If the call to create is successful, a Transaction.Created object is returned and assigned to the variable trc. If something goes wrong during the transaction's creation, an exception is thrown instead.

Now let's revisit the Transaction.Created object, which may look odd to you. This object is simply an instantiation of the Transaction interface's public inner class, which looks like this:

public static class Created implements Serializable {
 public final Transaction transaction;
 public final Lease lease;
 
 Created(Transaction transaction, Lease lease) {...}
}

This class is simple: it contains only two public fields and a constructor. This class is needed because the create call to the TransactionFactory needs to return two values -- the transaction itself and its granted lease time -- both of which the Created class wraps into one returned object. Once the call to create returns a Created object, you can simply access its two public fields (transaction and lease) to retrieve the respective objects. For instance, you can use the transaction field to obtain a reference to the newly created transaction object like this:

Transaction txn = trc.transaction;

Likewise, you can retrieve the transaction's lease by accessing the transaction's lease field. Note that a lease on a transaction represents the amount of time for which the transaction manager will maintain the transaction. Once a transaction expires, the transaction is aborted and all its participants are asked to roll back their state to the point before the transaction began.

An example

To demonstrate space-based transactions, we will show you how to write a simple space "relay" that takes the entries from a source space and copies them to a set of target spaces (removing them from the source space in the process). You do this in a transactionally secure manner, such that each entry is removed from the source space and then relayed to all target spaces in one indivisible "operation." Like most JavaSpaces applications, you don't need a lot of code to make this happen. Here's how you do it:

First, you define a simple Message class that you'll use to instantiate the entries that are relayed:

import net.jini.core.entry.Entry;

public class Message implements Entry { public String content;

// a no-arg constructor public Message() { } }

This is the same Message entry we used in Part 1 of the JavaSpaces series. The entry simply holds a content string and contains a no-arg constructor (recall from the first article that the no-arg constructor is needed by all entries for serialization purposes).

Now you write a method that populates your source space with a set of numMessages Message entries.

private void createMessages() {  
 for (int i = 0; i < numMessages; i++) {
  Message msg = new Message();
  msg.content = "" + i;
  try {
   sourceSpace.write(msg, null, Lease.FOREVER);
   System.out.println("Wrote message " + i + " to " + sourceName);
  } catch (Exception e) {
   System.err.println("Cant write message " + i + ": " + e);
  }
 }
}  

This method simply iterates numMessages times, instantiating a new Message entry (setting its content to a string that simply represents the loop iteration) and writing it into the source space each time through the loop. You won't need to make this process transactionally secure, since it is just used to preload the source space.

Next you write a method relayMessages that removes messages from the source space and copies them to a set of target spaces (represented by an array of spaces). This time you'll copy the messages in a transactionally secure manner.

private void relayMessages() {
 TransactionManager mgr = TransactionManagerAccessor.getManager();
 Message template = new Message();
 Message msg = null;
  
 for (int i = 0; i < numMessages; i++) {
  Transaction.Created trc = null;
  try {
   trc = TransactionFactory.create(mgr, 300000);
  } catch (Exception e) {
   System.err.println("Could not create transaction " + e);
   return;
  }
  Transaction txn = trc.transaction;
  
  try {
   try {         
    template.content = "" + i;
     
    // take message under a transaction
    msg = (Message)sourceSpace.take(template, txn, Long.MAX_VALUE);
    System.out.println("Took msg " + i + " out of " + sourceName);
     
    // write message to the other spaces under a transaction
    for (int j = 0; j < targetSpaces.length; j++) {
       targetSpaces[j].write(msg, txn, Lease.FOREVER);
       System.out.println("Wrote message " + i + " to " + targetNames[j]);
    }
   } catch (Exception e) {
    System.err.println("Can't relay message " + i + ": " + e);
    txn.abort();
    return;
   }   
   txn.commit();
  } catch (Exception e) {
   System.err.println("Transaction failed");
   return;
  }
 }    
}

The first thing you do in this method is call the getManager static method of the TransactionManagerAccessor class; as explained earlier, this returns a TransactionManager proxy object. Next you define two Message variables: one to serve as a template to match messages in your source space, and the other to hold entries you remove from the source space.

1 2 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more