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.
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.
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.
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.
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.
Next you enter a loop that iterates as many times as there are Message
entries in your source space. Each time through the loop, you first obtain a new transaction by asking the transaction manager to create one. You then relay a message as follows: First you create a template by setting a Message
entry's content field to hold the number of this loop iteration, for example "1" or "2" or "3" and so on. You then take the matching message entry from the space, being sure to specify the transaction txn
as the second parameter. Once you retrieve the entry, you loop through all the target spaces, writing the entry into each of them. Here again, you supply txn
as the transaction parameter in the write
method in order to have it operate under the transaction. Finally, when the loop is finished, you call commit
to complete the transaction.
Now we'll take a look at the exception handling in this code and show you that there are three ways the code can complete. Under one scenario, all your operations complete without throwing any exceptions. You call the transaction's commit
method, and it completes successfully. The effect is that all the write
operations take place in the space as one indivisible operation. On the other hand, the commit
itself might throw an exception. In this second scenario, the outer catch clause catches the exception and prints the message "Transaction failed," the transaction expires when its lease time runs out (in this case, after 5 minutes), and no operations occur in the space. Under the third scenario, the write
or take
operations themselves throw an exception, which gets caught by the inner catch clause, where you try to abort the transaction. If the abort succeeds, then the operations called under the transaction do not affect the space. Note also that the transaction will also expire if your JavaSpaces client terminates unexpectedly or gets disconnected from the network during the transaction.
Now that you've written these three methods that will do the bulk of your work, all that remains is creating the code that invokes them:
public SpaceRelay(String[] args) throws IOException {
// from the command-line arguments, get the names of the source space
// & target spaces, and the number of messages to relay
. . .
// obtain access to the source and target spaces
sourceSpace = SpaceAccessor.getSpace(sourceName);
targetSpaces = new JavaSpace[targetNames.length];
for (int i = 0; i < targetNames.length; i++) {
targetSpaces[i] = SpaceAccessor.getSpace(targetNames[i]);
}
// create the specified number of messages in source space
createMessages();
// relay messages from the source space to the target spaces
relayMessages();
}
public static void main(String args[]) {
if (args.length < 3) {
System.out.println("Usage: SpaceRelay source target1 target2 ... targetN numMessages");
System.exit(1);
}
try {
SpaceRelay spaceRelay = new SpaceRelay(args);
} catch (Exception e) {
e.printStackTrace();
}
}
As you can see, the main
method simply instantiates a SpaceRelay
object. The SpaceRelay
constructor obtains the names of the source and target spaces, as well as the number of messages to relay, from the command-line arguments. Then the constructor proceeds to obtain references to those spaces. Finally, the constructor calls the methods you created that populate the source spaces with numMessages
messages and relay those messages to the set of target spaces.
In this article, we've given you a peek at Jini's transaction model and explained specifically its use in the context of JavaSpaces. Jini transactions provide a general and powerful model for building robust distributed applications, particularly apps that need to operate safely and correctly in the presence of partial failure. While our examples used multiple transaction participants that all happened to be JavaSpace services, since Jini transactions are general you could just as easily perform operations on other Jini services under the same transactions. In addition, if you are creating Jini services, your service can implement the TransactionParticipant
API to be a willing participant in distributed transactions. For further study, we refer you to the resources below.
"Make Room for JavaSpaces, Part IV" by Eric Freeman and Susan Hupfer was originally published by JavaWorld (www.javaworld.com), copyright IDG, April 2000. Reprinted with permission.
http://www.javaworld.com/jw-04-2000/jw-0421-jiniology.html
Have an opinion? Be the first to post a comment about this article.
Dr. Eric Freeman, recently named one of the top 100 young innovators by MIT's Technology Review, is chief technologist of Mirror Worlds Technologies, a Java- and Jini-based software applications company, and a research affiliate in the department of computer science at Yale University. Eric recently coauthored JavaSpaces Principles, Patterns, and Practice, the official Sun Microsystems Jini Series book on the JavaSpaces distributed computing technology, along with Susanne Hupfer and Ken Arnold. Previously, Eric spent several years working closely with David Gelernter as a PhD student at Yale University on space-based systems (which are the progenitors of the JavaSpaces technology). Eric would like to thank Elisabeth Freeman and Susanne Hupfer for their careful reading, feedback, and comments on drafts of this article.
Dr. Susanne Hupfer is director of product development for Mirror Worlds Technologies, a Java- and Jini-based software applications company, and a research affiliate in the Department of Computer Science at Yale University, where she completed her PhD in space-based systems and distributed computing. Previously, she taught Java network programming as an assistant professor of computer science at Trinity College. Susanne coauthored the Sun Microsystems book JavaSpaces Principles, Patterns, and Practice.
Artima provides consulting and training services to help you make the most of Scala, reactive
and functional programming, enterprise systems, big data, and testing.