Survival of the fittest Jini services, Part 2

Understanding reliability in distributed transactions

1 2 3 4 Page 2
Page 2 of 4
  • Buying a book must be an atomic operation. Either the credit card is charged, the delivery is scheduled, and the book is taken out of inventory, or none of those operations should take place. In addition, we must also receive the PurchaseConfirmation object; otherwise, we won't know whether our purchase succeeded or failed.
  • Placing an order must leave all the services in a consistent state. This is specific to each service -- for instance, our credit card should not be overcharged, or a delivery should not be scheduled on a route a shipping company doesn't serve.
  • Each step in the purchase must be performed in isolation from other operations. Its results must be hidden from other operations until the transaction fully completes.

    Consider what would happen if the CreditCard service didn't offer the isolation property. Imagine that your credit card account has an available credit of 00, and the book you want costs 0. While you're placing the order, your spouse charges 80 to your account for a purchase at ABC Department Store. When the book order transaction begins, a charge for the 0 is made on the account, causing your available credit to shrink to 50. Right after this charge registers, ABC's request for the 80 charge is denied because of insufficient credit. However, during the book order transaction, it turns out that no shipping company delivers books to your desired destination. The purchase transaction is aborted, causing the credit company to cancel the 0 charge, which now restores the available balance to the original 00. When you inquire, you are told that your account has 00 available credit, and no one knows why the charge for the 80 was denied. (Someone could consult log files; however, that certainly wouldn't reveal why the 0 charge was reverted.)

    With transaction isolation, the credit card's balance would be inaccessible (locked) during the book order transaction -- the other company's charge request would have to wait. Locking the account balance trades transaction throughput for accountability: one service waits in line in order for our system to be more predictable.

  • The results of the purchase must be durable. The confirmation receipts and all other state changes in services must survive the transaction itself.

To guarantee these properties for the book purchase, each service must perform its operations under a transaction. In Jini terminology, the services must become transaction participants.

Towards transactional services

The first step for a service to become a transaction participant is to define transactional methods in its public service interface. Since Jini has to accommodate both transactional and nontransactional services, there is no equivalent of the transactional remote procedure call (TRPC) mechanism popular in traditional transaction processing systems. In those systems, the runtime infrastructure annotates each method call with a unique transaction identifier (TRID). By having an identical TRID, a set of operations are easily identified as belonging to (performed under) the same transaction.

Jini transactions group operations together via an object representing a transaction instance. For the default semantics, the net.jini.core.transaction.Transaction interface defines this object, which is implemented by net.jini.core.transaction.server.ServerTransaction. You need to pass in a Transaction instance along with the other parameters to make a method call transactional:

public interface BookStore {
  public Collection findBooks(Book template)
    throws RemoteException;
  public OrderConfirmation buyBook(Book book,
                                   Account creditCard,
                                   Customer customer,
                                   Address shipTo,
                                   int daysToDelivery,
                                   Transaction txn)
    throws NoSuchBookException, CreditCardException,
      DeliveryException, BookStoreException,
      RemoteException, TransactionException;
}

For the credit card service:

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 for the shipping service:

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; }

You will notice that each method now declares TransactionException to indicate possible failure in the transaction's processing. We still need the other application-specific exceptions for failures that occur independent of the transaction.

In addition to extending the method signatures, each service's implementation must also declare itself to be a transaction participant by implementing the net.jini.core.transaction.server.TransactionParticipant interface. By implementing this interface, the object guarantees that it can join the transaction and participate in the 2PC protocol.

Note that the service's implementation becomes a transaction participant; the service's proxy does not. The proxy runs inside the address space of whatever client retrieves it from lookup services. Therefore, the proxy's state becomes intrinsically a part of that client's state -- all computations performed on the proxy itself are local to the client.

A TransactionParticipant service must join a transaction when it receives a transactional method call. The ServerTransaction class provides a join() method that consumes a TransactionParticipant and a crash count. Your service can join the same transaction with the same crash count any number of times. All the method calls that join the transaction perform their actions under that transaction.

During a transaction, your service might crash for some reason. If such a crash causes your service to lose the changes made during the transaction, you must increase the crash count number when the service reactivates and reinitializes. Since the default semantics must guarantee ACID properties, joining a transaction with a crash count number different from the service's original crash count results in a CrashCountException. At that point, you can decide what to do: you might choose to abort the whole transaction.

Transaction lifecycles

Once all the bookstore services are transactional, you can at last order your favorite book. The transaction client is the Jini service that initiates the transaction. The client might or might not also be a participant in the transaction. Since printing or displaying the PurchaseConfirmation is part of the book purchase transaction, we will make the BookStore service both a client and a participant.

The client follows these steps to initiate a new transaction:

  1. It discovers the transaction manager service. Since it's just a regular Jini service, you can follow the normal Jini service discovery mechanism.
  2. Since different objects could represent various transaction semantics, you create a Transaction via a factory class. Like many Jini entities, a transaction is a leased resource. Calling TransactionFactory's create() method produces a ServerTransaction.Created object, which bundles a new transaction with its lease.
  3. If the client is also a transaction participant, it can at this point join the transaction.
  4. It then passes the transaction object as a parameter in method calls to other services.

A new transaction starts out in the active stage. In this stage, the services perform their work under the transaction. For instance, the credit card service charges your account, the bookstore service locates and queries the shipping services, and a package delivery is scheduled. Finally, the bookstore service must produce a purchase confirmation. During these activities, all three services must be ready to roll back any changes they make, since the whole transaction's success is not yet guaranteed.

At some point, the client (or any other participant, for that matter) indicates that the transaction must complete. With our bookstore, this might occur right after we've displayed or printed the order confirmation, or after we've waited a set amount of time for the services to finish their work. Then the 2PC protocol drives the transaction to completion.

The transaction manager coordinates the transaction's commitment. The client (or any other participant) calls the commit() or abort() methods on the Transaction object. This in turn causes the manager to call the prepare() methods on each participant.

At this point, the transaction enters the voting stage. Each participant must vote: Is it prepared to roll forward the transaction's changes, does it need to abort the transaction, or does it not care either way (because the transaction caused no changes in its state)? The participants' possible votes are PREPARED, ABORT, or NOTCHANGED. Most significant, if any participant cannot ensure its transactional guarantees, it must indicate that fact. For example, the credit card service might not be able to save the new credit card balance.

When a participant votes PREPARED, it says, in effect, that I am now committed to the changes made under the transaction. This implies that, given the order to roll forward, the participant guarantees to commit the changes -- it cannot fail. Among other things, this means that the changes have already been saved in persistent store (to guarantee the transaction's durability property).

When, and only when, all participants vote either PREPARED or NOTCHANGED, the coordinator calls the commit() method on each participant. When all participants commit their changes, the transaction is in the COMMITTED state and can thereafter be forgotten. (Transactions typically don't persist after they've completed, although the spheres of control notion I mentioned in the first part of this series assumes that they do, which opens up many interesting possibilities.)

The commit() call instructs a participant to finish the transaction, which means that the participant no longer needs to enforce the transaction guarantees. The results of the changes made during the transaction now become visible to objects outside the transaction, locks held by the transaction are released, and so forth. The commit() method doesn't have a return value, since a PREPARED vote previously implied a guaranteed successful commit.

If any participant votes ABORT, the transaction manager calls the abort() method on all participants, instructing them to roll back all changes made during the transaction and release any resources they've reserved.

In this sense, the transaction provides a set of computation guarantees: if any participant decides that it cannot, for some reason, abide by the transaction's semantics, the entire computation will be cancelled rather than produce an unreliable result and unpredictable side effects.

Figure 3 shows the transaction's different states during the 2PC protocol from the transaction client's point of view:

Figure 3. The client's view of a transaction

Figure 4 illustrates the interaction between a participant and a transaction:

Figure 4. The participant's view of a transaction

Finally, Figure 5 illustrates how a manager drives the transaction to completion:

Figure 5. The manager's view of a transaction

Putting it all together

The following code illustrates the purchase of a book under a transaction:

1 2 3 4 Page 2
Page 2 of 4