Survival of the Fittest Jini Services, Part III

Implement Transactional Jini Services

by Frank Sommers
October 19, 2001

First published in JavaWorld, October 19, 2001
Summary
The Jini Transaction Specification defines a transaction coordinator for the two-phase commit protocol, as well as a default transaction semantics guaranteeing ACID properties. This article explains the default transaction semantics, and offers guidelines for its implementation in services based on activatable RMI objects.

In my last Jiniology article, I described distributed transactions that enable multiple Jini services to reliably coordinate their work to support a common objective: When services enroll in a transaction, they guarantee that any outcome agrees with a set of conditions, or invariables.

For example, an online bookstore, implemented as a Jini service, uses a credit card service for payment processing. By enrolling the credit card service in a transaction, a purchase guarantees that either the credit card is charged and the book ships, or that neither action takes place.

A transaction promises certain computation invariables that define the transaction's semantics. Jini lets you implement any transaction semantics. You can decide what guarantees cooperating services must provide, and implement your services according to those guarantees.

Regardless of specific invariables, you can use the Jini transaction coordinator to arrange your transaction's commitment, employing the two-phase commit protocol. In Part 2 of this series, I described how you use that coordinator as a client for the two-phase commit protocol. In this article, I will focus on techniques for writing transaction participant Jini services.

Read the whole "Survival of the Fittest Jini Services" series:

Jini, Transaction Monitors, and Application Servers

While Jini affords you the flexibility to implement any type of transaction semantics, you want to ensure that, at the minimum, transactions preserve your services' information integrity. To this effect, the Jini Transaction Specification describes a default transaction semantics, guaranteeing the ACID properties: atomicity, consistency, isolation, and durability.

As you shall see, you need the ACID properties to preserve the integrity of shared data. Most transaction processing systems -- including those used in many J2EE (Java 2 Platform, Enterprise Edition) application servers -- automatically enforce these guarantees. Commonly, you can enforce transaction semantics by maintaining containers in which objects can execute during transactions. You can then make these containers ensure transactional semantics for the objects they manage. A significant portion of a J2EE application server's code maintains container objects and their relationships to business-specific Java objects. At the price of increased system complexity, automating transaction management offers you convenience. This convenience is one reason for the success of transaction processing (TP) monitors, as well as J2EE application servers.

Most programmers are familiar with transaction processing in database management systems (DBMS) as well as application servers. Commercial DBMS products often provide an API for accessing transactions from client programs. In the JDBC (Java Database Connectivity) API, for example, the java.sql.Connection interface lets you control a transaction's commitment via the commit() and rollback() methods. When programming with JDBC, you don't have to know how the underlying DBMS guarantees a transaction's semantics -- the DBMS automates that task for the objects (records) it manages, and the API acts as a facade to that functionality.

A transaction-aware Jini service provides a similar facade via its transactional method signatures. In addition to other method parameters, a transactional method consumes an object representing a transaction instance. That object, in turn, signifies a specific transaction's semantics. When the object representing a transaction is of type net.jini.core.transaction.Transaction, it signifies the default transaction semantics prescribed by the Jini Transaction Specification.

Why Commitment Must Precede Trust

To illustrate the default transaction semantics' goals, let's revisit the Jini bookstore's service interface introduced in Part 2 of this series:

public interface BookStore {
  public Collection findBooks(Book template)

    throws RemoteException;
  public OrderConfirmation buyBook(Book book,

                                   CreditCard card,

                                   Customer customer,

                                   Address shipTo,

                                   int daysToDelivery,

                                   Transaction txn)

    throws NoSuchBookException, CreditCardException,

      DeliveryException, RemoteException, TransactionException;

}

The bookstore uses two other Jini services for credit card processing and shipping. These services have the following interfaces:

public interface CreditCard {
  public ChargeConfirmation debit(Account account,

                                  Charge charge,

                                  Transaction txn)

    throws NoSuchAccountException, CannotChargeException,

      CreditCardException, RemoteException, TransactionException;
  public PaymentConfirmation pay(Account account,

                                 Payment payment,

                                 Transaction txn)

    throws NoSuchAccountException, CreditCardException,

      RemoteException, TransactionException;
public CurrentBalance getBalance(Account account)

    throws NoSuchAccountException, CreditCardException,

      RemoteException;


and

public interface ShippingCompany {
  public PickupGuarantee checkPickup(Address origin,

                                     Address destination,

                                     PackageDesc package,

                                     int daysToShip)

    throws ShippingException, RemoteException;
  public PickupConfirmation schedulePickup(PickupGuarantee guar,

                                           Transaction txn)

    throws NoSuchGuaranteeException, ShippingException,

       RemoteException, TransactionException; }

Regardless of any particular implementation, these services provide interfaces to persistent information. While some method calls in the service interfaces need read-only access to persistent data, other method invocations modify data. For instance, the CreditCard service's getBalance() method only reads the credit card's current balance. The debit() method, on the other hand, modifies persistent data -- it increases the account's balance, and can log a purchase onto persistent store.

You can imagine service-performed operations on persistent data in terms of read and write actions. While transactions are units of state transformation (write actions), their importance extends to read actions as well. You can assume outputs from transaction-performed actions only when the transaction commits. This notion is key to understanding the default transaction semantics for Jini services. To solidify this notion, consider the following steps in arranging a book payment:


Figure 1. Perform a transactional method invocation in Jini

Figure 1 illustrates these steps:

  1. A customer purchases a book by calling BookStore's buyBook() method. BookStore then discovers and obtains a reference to a CreditCard service.
  2. BookStore next calls a CreditCard service's debit() method, passing in the customer's account information.
  3. Because debit() consumes a Transaction object, CreditCard implements the net.jini.core.transaction.server.TransactionParticipant interface. TransactionParticipant is a remote interface; it extends java.rmi.Remote, making its methods available for calls from remote virtual machines.
  4. With a call to debit(), CreditCard joins the transaction instance passed in as a parameter. It first casts the Transaction parameter to a net.jini.core.transaction.server.ServerTransaction; then it calls the join() method on that object, passing a reference to itself (a TransactionParticipant).
  5. As a result of joining the transaction, the transaction manager registers CreditCard as a transaction participant. When the transaction commits, the manager calls the commit() method that TransactionParticipant mandates and an instance of CreditCard implements. If the transaction aborts, then the transaction manager calls abort() instead of commit.

In the simplest terms, BookStore calls CreditCard's debit(), waits for a ChargeConfirmation, and then calls commit() in the Transaction object. Thus, although debit() returns a ChargeConfirmation, that ChargeConfirmation cannot be relied upon until the transaction commits.

Therefore, there are two interaction levels between CreditCard and BookStore: method invocation and a transaction. Method invocation outputs are not reliable -- not final -- until the transaction commits. This is because a transaction is an indivisible set of actions, performed atomically; either all transaction steps succeed, or it must appear to the user that the transaction never took place. For instance, if your credit card charge is accepted, but then the shipping company fails to confirm a book's delivery, you don't want that credit card charge to take effect. In that case, the transaction aborts, causing the credit card charge to appear as though it never took place.

Concurrent transactions

As long as only one transaction executes on the network at any given time, you can easily provide this guarantee: All transaction steps perform consecutively, with no one else permitted to read or write the data those steps access. When one transaction completes before another begins, the transactions execute serially.

To illustrate this, imagine another transaction that uses the credit card service but is unrelated to the book purchase -- a transaction that arranges your utility bill payment using your credit card, for example. This transaction transfers money between two accounts: your credit card and the utility company's bank account. It coordinates work (the transfer) between CreditCard and a utility account service, Utility. Let's designate this transaction as TPayUtility, and denote the original book purchase transaction as TBuyBook.

Figure 2 shows the steps of these two transactions when all TBuyBook steps complete before any TPayUtility steps begin (i.e., when the two transactions execute serially).


Figure 2. Serial execution of two transactions

Neat as this arrangement appears, the Jini federation cannot ensure a purely serial transaction execution. Doing so requires a central controller -- a transaction scheduler -- that tracks the beginnings and ends of transactions, causing others to wait to begin until a transaction executes all its steps. In addition, forcing serial transaction execution severely limits the network's ability to execute transactions on time -- the network's transaction throughput -- as most transactions are delayed. Transactions that don't execute sequentially can easily violate the consistency of shared data. To see how this could happen, we must first change our vantage point of what occurs inside a transaction.

Thus far, we've taken a birds-eye view. We've considered the distributed transaction's actions in all the nodes (services) with which it interacts. However, in a distributed system without a central controlling entity, that is an artificial viewpoint. Any service involved in a transaction is aware only of what read and write operations occur locally, and has no way to gain a global perspective on a transaction. Therefore, we will change our observation point and look at the serial execution of two transactions as they interact with just one service, CreditCard.

Viewed inside CreditCard, TBuyBook first reads the credit card's existing balance as well as the maximum allowed balance. If the purchase falls below the maximum limit, the transaction then increments the current balance, and writes that amount back to stable storage. If the purchase of the book would push the card's balance over the credit limit, the current balance is left intact; in that case, the service returns a CreditCardException from the method call. TPayUtility works similarly: It begins by reading the account balance and maximum allowed credit limit, and determines if it can charge the utility bill on the card. If so, it increments the balance, and writes the new amount to persistent store. Figure 3 illustrates the performance of these transactions inside CreditCard.


Figure 3. Read and write steps performed in CreditCard when two transactions execute serially

While these actions occur, each transaction interacts with other services, of which CreditCard is not aware and any of which can cause the transactions to abort. For instance, after TBuyBook-related actions complete inside CreditCard, the ShippingService might cause the transaction to abort for lack of available shipping route. At that point, CreditCard must restore the old balance. Therefore, the transaction's abort will appear as a new write operation inside CreditCard.

Figure 4 shows four possible sequences of read and write operations, considering a transaction abort as a new write action. The four possible execution histories show what can go wrong when actions from concurrently executing transactions interleave.


Figure 4. Concurrently executing transactions introduce possible anomalies: lost updates, dirty reads, and unrepeatable reads. If both transactions only read the data, no anomalies occur.

You can easily imagine what goes wrong in H1, H2, and H3 with an example. Suppose you purchase the book for $30, and the Jini service pays the $50 utility bill. Your initial account balance is $100. Based on these initial facts, concurrently executing transactions would introduce the following anomalies:

  1. Lost updates: In H1, one transaction (TBuyBook) reads the account balance, followed by another transaction (TPayUtility) reading the balance. At that point, both transactions know that the account balance is $100. Next, TBuyBook increments the balance, adding the book's price, and writes the new value, $130, to stable store. Finally, TPayUtility computes the balance, adding the utility bill price to the old balance, and writes its value to stable store ($150). Thus, the book purchase price is lost -- in effect, you receive a free book.
  2. Dirty reads: In this scenario, TBuyBook succeeds in setting the new balance to $130. Following this, TPayUtility reads this balance, and determines that the new balance, with the utility bill added, should be $180. At that point, however, TBuyBook aborts, reverting the balance to $100. That $130 was dirty data -- that is, data changed by a transaction (TBuyBook) that had not yet committed. TPayUtility should not have seen the $130 until TBuyBook committed. If your credit limit was less than $180, TPayUtility would fail, even though the actual account balance never reached that amount.
  3. Unrepeatable reads: If a transaction reads a value, and then another transaction changes that value, when the first transaction tries to reread that data, it gets a different value, even though it never changes that data item. For instance, if TBuyBook reads the account balance, and then TPayUtility changes that balance, TBuyBook's reread produces a different amount, even though TBuyBook never changes the data.

You can avoid these anomalies, and ensure consistency, by making concurrently executing transactions appear as if they were executing serially. In other words, from the Jini federation's point of view, transactions might execute concurrently, but from a service's vantage point, transactions should appear to execute one after another, in isolation (the I in ACID). As long as each service ensures transaction isolation, concurrently executing transactions won't threaten data consistency.

The Two-Phase Locking (2PL) Protocol

The most common technique in isolating a transaction's execution is for a service to lock data that the transaction reads or writes. Before each read operation, the service places a read lock on an object, and before each write operation, it places a write lock on an object. This way, the read and write operations are covered by locks.

There are a few simple rules for managing locks on objects. First, locks must be held until the transaction commits or aborts. Recall that an abort corresponds to a write operation, replacing the new value with the old one. In case of an abort, therefore, after that final write restores the old value, the write lock releases on the modified object. A transaction is well formed if each of its operations is covered by locks, and if all locks release at the transaction's completion.

Second, since an object with a read lock is not modified by a transaction, other transactions can read that object, even if the first transaction is still in progress. For this reason, read locks on an object can be shared between transactions -- they are shared locks.

On the other hand, if an object has a write lock, that implies that the lock-owning transaction modified the object. A write lock is exclusive to a transaction that owns it -- other transactions cannot share it. If an object has an exclusive lock on an object, other transactions must wait before they can either read or write that object. In addition, a transaction cannot lock an object if it does not intend to perform a subsequent read or write on that object.

And finally, a transaction can upgrade a shared lock to an exclusive lock, but cannot downgrade an exclusive lock to a shared lock. Doing so would allow other transactions to read dirty data.

Figure 5 shows two transactions, TBuyBook and TPayUtility, performing lock-covered read and write actions.


Figure 5. Two transactions ensure that their read and write operations are covered by, respectively, shared and exclusive locks.

Because a distributed transaction can abort even after its write operations complete in a service, that service must hold on to all locks associated with a given transaction until that transaction either commits or aborts. In addition, after a transaction releases any lock, it should never acquire new locks on any object it reads or writes.

In other words, your Jini service goes through two distinct steps to manage locks: When it receives a transactional method invocation and joins the transaction, it acquires locks on objects the transaction reads or writes. Then, when the transaction's commitment or abort is called, it starts releasing all those locks. Once the release phase starts, your service cannot acquire new locks on that transaction's behalf. This lock-management strategy is called the two-phase locking protocol (2PL). Figure 6 illustrates this protocol.


Figure 6. The two-phase locking protocol. In the first stage, a service acquires all the locks on a transaction's behalf; in the second, it releases the transaction's locks.

The 2PL protocol (see Jini Transaction Specification, Section 3.5) provides the default transaction semantics for Jini services, preserving the transaction's ACID properties:

Transaction semantics for objects are defined in terms of strict two-phase locking. Every transactional operation is described in terms of acquiring locks on objects; these locks are held until the transaction completes. The most typical locks are read and write locks, but others are possible. Whatever the lock types are, conflict rules are defined such that if two operations do not commute, then they acquire conflicting locks. For objects using standard read and write locks, read locks do not conflict with other read locks, but write locks conflict with both read locks and other write locks. A transaction can acquire a lock if the only conflicting locks are those held by ancestor transactions (or itself). If a necessary lock cannot be acquired and the operation is defined to proceed without waiting for that lock, then serializability might be violated.

Intuitively, you can easily see that transactions with participants that obey the 2PL rule prevent dirty or unrepeatable reads, as well as lost updates; therefore, they execute in isolation from other concurrently running transactions. In other words, well-formed transactions with the 2PL protocol guarantee isolation. Several mathematical proofs exist to show that this rule is true for any transaction (see Jim Gray's Transaction Processing: Concepts and Techniques). Isolation, in turn, is a prerequisite for preserving data consistency (the C in ACID).

Although in an abstract sense a transaction owns locks on objects, your service manages those locks. Managing locks, and associating them with transactions, is a transactional Jini service's main task.

Transactional Resource Managers

I began this article by contrasting systems that automatically enforce transactional semantics, such as database management systems and application servers, with Jini. From a distributed transaction system's viewpoint, these are transactional resource managers (TRM). If you implement a Jini service such that a DBMS or an application server manages all persistent data accessed under a Jini transaction, you can delegate lock management to that TRM.

Because TRMs manage much of the world's data, the Open Group, a technological consortium, developed the XA and XA+ standards, defining the interaction between a TRM and a transaction coordinator (see Resources for more information on these standards). Many commercial products, including DBMSs like Oracle or IBM's DB2, are TRMs that provide an XA interface. The javax.transaction.xa package, a standard part of J2SE (Java 2 Platform, Standard Edition), allows an XA-compatible TRM to interact with Java components, including Jini services. That package's XAResource interface defines methods for a resource to commit or abort (roll back) a transaction, and also facilitates steps of the two-phase commit protocol I described in Part 2.

While existing implementations of the Jini transaction coordinator don't directly allow an XA resource to enroll in a Jini transaction, you can implement a Jini service such that it integrates an XA transaction with a Jini transaction by mapping one to the other (perhaps using both transactions' unique IDs). Apart from this Jini-to-XA mapping, such a Jini service would not have to manage locks on objects, since that is then delegated to the XA resource.

Given that a variety of software systems -- including, lately, application servers -- expose XA-compatible interfaces, transactional interaction via this standard is becoming increasingly important. You could implement a Jini transaction coordinator so that it also enrolls XA resources in a transaction. That is an area ripe for progress in the Jini universe.

JavaSpaces are another resource manager that provides transactional access to objects. Their advantage is that they seamlessly integrate with Jini transactions. If you can manage data accessed by your transactions inside JavaSpaces, then you can delegate lock management on those objects to a JavaSpaces implementation. In a previous Jiniology article, Eric Freeman and Susanne Hupfer describe how you can enroll JavaSpaces in a Jini transaction.

Finally, although JDBC offers access to an underlying system's transaction mechanism, it does not directly support distributed transactions. If your Jini service stores its data in a DBMS that you access via JDBC, your DBMS won't be able to ensure transaction isolation during all phases of the two-phase commit protocol. While JDBC provides access to starting and committing a transaction inside a DBMS, it doesn't directly facilitate the prepared stage of a distributed transaction. Recall that when a participant votes PREPARED in a Jini transaction, it guarantees that it can roll forward if it receives a subsequent commit(), even if the service crashes during the transaction. Since JDBC does not facilitate this PREPARED vote, your Jini service relying on JDBC to ensure a transaction's integrity and its associated data cannot offer this vote with confidence.

Activatable RMI Objects and Transactions

For simpler Jini services, you might decide that an external transactional resource manager would be an overhead. Since many Jini services are implemented as activatable RMI (Remote Method Invocation) remote objects, I will conclude this article with an example of how such an object might manage a transaction's locks.

In this example, the CreditCard service manages accounts in virtual memory, and accesses them via a Map data structure. This data structure associates an account number with an object representing the account, CCAcct. Each CCAcct, in turn, maintains a list of past purchases.

Here are the key objectives for this example:

  • The service should be able to handle concurrently running transactions. It needs to track the objects that each transaction has read or written, and place appropriate locks on those objects. It should hold those locks until the transaction commits or aborts, at which time it should release those locks.
  • Transactions that wish to access data locked by another transaction must wait. You accomplish this by causing the thread executing the transactional method invocation to block; the thread is notified when locks on the desired objects release.
  • Locks must survive service crashes. This is necessary, since a transaction itself may not abort if one of its participants crashes. When that participant crashes, it must return to its exact state before the crash, including holding all the locks it managed for objects.

This simple lock manager centers on a data structure that tracks the relationship between transactions, objects, and locks. In implementing 2PL, we aim to properly manage this data structure. In addition, we must guarantee the data structure's persistence and recovery in the event of service crashes.

The net.jini.core.transaction.server.ServerTransaction class, an implementation of Transaction, provides a unique transaction ID. We can use that ID as a key to unique transaction instances, and to map the objects those transactions access.

For allocating locks on objects, the JVM's monitor-based locks are not sufficient. Threads, not transactions, own monitor locks, and there is no necessary correspondence between Java threads and transactions. (Intuitively, you can imagine transactions as execution threads spanning not only virtual machines, but also possible VM restarts.) Therefore, in this example, SLOCK and XLOCK objects represent shared and exclusive locks, respectively.

You can download the code for this simple example lock manager from the URL specified in Resources. Note, however, that the data structure managed in this example is relatively simple. With more complex data structures, lock management can become far more involved.

Jini Philosophy

If lock management is a key aspect of a transaction-participant Jini service, and if it is so difficult, you might ask, "Why doesn't Jini aim to automate this chore?" This area is open for discussion in the Jini community; I encourage you to contribute to this ongoing debate.

In my opinion, the reason why Jini doesn't provide an infrastructure for container-managed transactions, or some other automated way of ensuring transaction semantics, cuts directly to the heart of how Jini differs from application servers and other Java distributed enterprise technologies. While TP monitors and app servers provide particular services on the network, Jini itself is an infrastructure that ties all such services together reliably and preserves their dependable interaction even in the face of constant network change.

By divorcing services from servers, Jini places the network at the center of a distributed system. In a Jini federation, the network provides particular services -- not individual nodes, or specific software components. Shifting a system's design center to the network lends a Jini system enormous scalability and resilience in the presence of partial failures and network change. However, programmers must realize that no container can hold the network as such.

Further, a Jini federation does not assume the continuous presence of any centralized controlling entity on which any Jini service can depend. This is true even for lookup services: Jini does not assume the continuous presence of a particular lookup service in a federation; instead, it mandates that services continuously discover lookup services, and register with those that advertise appropriate lookup groups.

For transactions, not assuming a continuously present controlling entity implies that ensuring transaction semantics remains each service's responsibility. In other words, Jini does not hide a service's transactional nature -- it makes that explicit on the network, and delegates implementation to the service. This is analogous to the way Jini doesn't hide the possibility of partial network failures; it declares that possibility explicitly (for example, in terms of leases and remote exceptions), and leaves it up to each service to handle partial failure conditions. Finally, a similarity exists also in Jini's making concurrency on the network explicit: Jini services are accessed concurrently, and every implementation must explicitly address concurrency needs.

Transactions, partial failure, and concurrency are characteristics of any networked environment. While papering over these network features might make programming initially easier, it also can reduce a system's stability in the long run.

The reason has as much to do with philosophy as with software systems. As the English philosopher Francis Bacon said, "Nature to be commanded must be obeyed." In other words, we must recognize that things have particular, identifiable characteristics, and that those characteristics determine how things interact with their environment. If we are to command something to our benefit, we can only do so if we obey and respect its characteristics. But to do that, we must first make those characteristics explicit, and then base our interaction with the thing on those characteristics. In essence, Jini's design philosophy could be summarized with a paraphrase of Bacon's dictum: "A network to be commanded must be obeyed."

Resources


"Survival of the Fittest Jini Services, Part I" by Frank Sommers was originally published by JavaWorld (www.javaworld.com), copyright IDG, October 2001. Reprinted with permission.
http://www.javaworld.com/javaworld/jw-10-2001/jw-1019-jiniology.html

Talk back!

Have an opinion? Be the first to post a comment about this article.

About the author

Frank Sommers is founder and CEO of Autospaces, a company focused on bringing Jini technology to the automotive software market. He also serves as VP of technology at Los Angeles-based Nowcom Corp., an outsourcing firm. He has been programming in Java since 1995, after attending the first public demonstration of the language on the Sun Microsystems campus in November of that year. His interests include parallel and distributed computing, the discovery and representation of knowledge in databases, and the philosophical foundations of computing. When not thinking about computers, he composes and plays piano, studies the symphonies of Gustav Mahler, or explores the writings of Aristotle and Ayn Rand. Frank would like to thank Bob Scheifler, a Sun Microsystems distinguished engineer and member of Sun's Jini team, for his comments and clarifications on Jini transactions.