Survival of the fittest Jini services, Part 3

Guidelines for implementing the Jini Transaction Specification in web services

The Jini Transaction Specification defines a transaction coordinator for the two-phase commit (2PC) protocol, as well as a default transaction semantics guaranteeing ACID properties. This article explains the default transaction semantics based on the two-phase locking (2PL) protocol, 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.

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.

1 2 3 Page 1
Page 1 of 3