ICS214B Project: Distributed Travel Reservation System

Index

Project Description
The goal of the project is to construct a distributed application in Java that implements a simple travel reservation system. You'll implement the project in two stages: in Part 1 (DUE: Thurs, March. 2), you'll implement a simple Resource Manager (i.e., a very simple database system with a fixed set of tables and operations) that supports concurrent transactions with ACID properties. In Part 2 (DUE: final exam day) you'll implement a Workflow Controller and a Transaction Manager that enable distributed transactions across several Resource Managers.
Part 1: Simple Travel Resource Manager (March 2nd 11:59pm)
Implement a simple Resource Manager (RM) that supports concurrent transactions with ACID properties. Specifically, the RM stores data about flights, rental cars, hotel rooms, and customers. Multiple clients access this RM concurrently to query and update the data through a transactional interface. It is the responsibility of the RM to ensure that these concurrent transactions are executed correctly, i.e., in accordance with the ACID requirements.

Conceptually, the RM stores the following tables:

FLIGHTS(String flightNum, int price, int numSeats, int numAvail)
HOTELS(String location, int price, int numRooms, int numAvail)
CARS(String location, int price, int numCars, int numAvail)
CUSTOMERS(String custName)
RESERVATIONS(String custName, int resvType, String resvKey)

To keep things simple, we'll make some assumptions:

  • There is only one airline, and all seats on a given flight have the same price; flightNum is a primary key for FLIGHTS.
  • All hotel rooms in a given location cost the same; location is a primary key for HOTELS.
  • All rental cars in a given location cost the same; location is a primary key for CARS.
  • custName is a primary key for CUSTOMERS.
  • The RESERVATIONS table contains an entry corresponding to each reservation made by a customer for a flight, car, or hotel, as follows: resvType indicates the type of reservation (1 for a flight, 2 for a hotel room, and 3 for a car), and resvKey is the primary key of the corresponding reserved item.
  • In the FLIGHTS table, numAvail is the number of seats available to be booked on a given flight. For a given flightNum, one of the database consistency conditions is that the total number of reservations for a flight (in the RESERVATIONS table) plus the number of available seats must add up to the total number of seats in the flight. Similar conditions hold for the CARS and HOTELS tables.
We provide the standard interface for the ResourceManager (ResourceManager API). Among other things, the interface allows rows to be added, deleted, and modified for each of the tables the RM stores. Your job is to write the implementation for the interface we provide.

The RM supports transactions. A transaction calls the start() method of ResourceManager to get a unique transaction id. All subsequent calls to the RM include the transaction id. Finally, the transaction calls commit() or abort(). A typical client transaction might look something like the following: (Of course in reality you should be checking for return values and catch exceptions... Also look at Client.java for another simple example.)

int xid = rm.start();
// If it's cheap enough and there're seats left
if (rm.queryFlightPrice(xid, "214") < 300 &&
    rm.queryFlight(xid, "214") > 0) {
  rm.reserveFlight(xid, "John", "214");
}
rm.commit(xid);   

Concurrency control is done through two-phase locking. In particular, when a client requests to query or update information, your RM implementation is responsible for locking things appropriately, and releasing all locks only when a transaction commits or aborts. We have provided a lock manager to help with this task. The lock manager supports the following API (this is pseudo-code, the actual LockManager API is here):

  • lock(xid, thingBeingLocked, read|write) throws DeadlockException
  • unlockAll(xid)
As before, xid is the unique transaction identifier. The lock manager handles deadlocks by a timeout mechanism (currently set at 10 sec). A failed (deadlocked) lock operation throws a DeadlockException. The lock manager also handles lock conversion (also known as lock upgrade). Lock upgrade happens when a transaction that has a read lock on an item needs a write lock on the same item.

lock(xid, foo, read); // read foo
lock(xid, foo, write); // write foo

Other transactions may have read locks on foo, so deadlock is possible when you upgrade locks.

Getting Started

Your code is expected to work on the ICS SUN machines. You are free to work on any other machines, including your own PC, where you can find a Java environment. However, before you submit, you should test and make sure you code compiles and runs on ICS UNIX machines, because that's where we will be testing it.

The supplied codebase (API) has two packages: lockmgr and transaction. The lockmgr package implements the class LockManager (API). You won't need to modify the contents of this package. The transaction package has some basic classes to get you started with your implementation. It contains the interface ResourceManager. Note that you are NOT to modify this file. Your implementation of the RM goes into the class called ResourceManagerImpl (plus additional new classes that you define as necessary), which implements the ResourceManager interface. The current ResourceManagerImpl.java contains a toy implementation which you will replace.

The transaction directory also contains a simple client in Client.java. During your implementation, you may want to write more sophisticated clients to test against your RM, but you're not required to turn these in. In fact, for grading, we will link your code with our own version of Client.java and see if you RM handles all cases correctly.

The client communicates with the RM via Java RMI (Remote Method Invocation). You can find a tutorial on RMI here. However, our toy ResourceManagerImpl and Client already have all the necessary RMI code for this part. The only thing you have to do is to modify the first line in transaction/Makefile to give yourself a unique port number for the RMI registry, so that you don't conflict with other ICS214 students who happen to work on the same machine as you (see later).

You will turn in all your source files plus a README file for submission. Grading will be based largely on correct functionality. The readme file should meet the following requirements: (1) Briefly explain your design and implementation. It should include enough details for the grading. (2) Specify in what (ICS) machine you tested your code. (3) Write the file in about 2-3 pages. In addition, you should make sure that your code should work following the instructions in MY README.txt file. Don't send me your own instructions to test the code, since each group has a submission.

Steps to get started:

  1. Download the tarball from here
  2. Put the tarball in a directory you've created for this class, say, ~/ics214b
  3. cd ~/ics214b
  4. tar xvf part1.tar
  5. cd project/lockmgr
  6. make
  7. cd ../transaction
  8. Think of a random number between, say 2000 and 5000, and change the value of RMIREGPORT in first line of Makefile from 1099 to this number. This is the port number where your rmiresgistry will be listening. Giving it a random number will ensure that your rmiresgistry doesn't conflict with somebody else's.
  9. make
  10. To run the system, first start the server (ResourceManager) by: make runserver. When you see "RM bound", the server is up and ready to take requests.
  11. Then, open another shell window on the same machine, go to the transaction directory, and do: make runclient
  12. When you're done, Ctrl-C to kill the server

Implementation hints

Here is a suggested approach to implement the required functionality in an iterative manner (note that the steps below may not all require the same programming effort; some steps are harder than others):
  • Step 1. Basic RM. Ignore atomicity to start with. Ignore persistence and assume that the data is stored in memory. One possible implementation is to store the data in one or more hashtables. You could have one hashtable per database table, or combine all database tables into a single hashtable. Each row in a table would then be a hashtable entry, keyed by the row's primary key. You might consider using a Java class to represent each kind of row e.g., a class corresponding to a FLIGHT row might have data members for flightNum, price, numSeats, and numAvail, and perhaps some additional meta-data needed to implement transactional properties.

    The RESERVATIONS table doesn't have a primary key, but it's mostly looked up by the custName; so one possible implementation is to combine it with the CUSTOMERS table. That is, have a hashtable indexed by custName, with each hashtable entry containing a list of (resvType, resvKey) pairs.

    Note that in our simple data model, you can have only one price per location, so the net effect of

    addCars(T1, "San Diego", 4, $52);
    addCars(T2, "San Diego", 7, $54);
    
    should leave 11 cars at $54, not 7 cars at $54 and 4 cars at $52.

  • Step 2. Atomicity. Now add atomicity to the RM using shadowing. Keep two copies of the in-memory database, with a pointer to the active copy. Keep each active transaction's updates in a separate hashtable. When the transaction commits, merge the transaction's updates into the non-active database copy and then swap the active database pointer. Shadowing will be explained in class.

    In the current system, the only failure to handle is an abort. Since the memory image is lost when the process terminates, there doesn't need to be any recover() method at this stage.

  • Step 3. Isolation. The RM should lock data appropriately for each transaction, and unlock data when the transaction commits or aborts. You might experiment with different locking granularities at this stage. In general, we would prefer that you lock data at the finest granularity possible for any operation, thus ensuring the maximum concurrency. The RM needs to handle deadlocks properly: when a transaction is deadlocked (as indicated by DeadlockException thrown by the LockManager), the RM should forcibly abort the transaction.

  • Step 4. Durability. Add persistence and recovery to the Resource Manager. All state is stored on disk. (You might find Java's java.io.ObjectOutputStream class handy; it allows you to output a stream of objects to a file on disk and the corresponding ObjectInputStream can read the objects back into memory). The disk image is updated when a transaction commits. Shadowing is used to ensure atomicity: two copies of the database are maintained on disk, as well as a pointer to the active database. On startup, the RM must implement a recover() method to restore its state from the state on disk, and gracefully handle various exceptions, such as operations called with unknown (or forgotten) transaction ids.

Testing
We will test the Simple RM using multiple clients and a single resource manager. The Technical Interface methods in ResourceManager are defined to make it easier to test for faults. The shutDown() method implies that the RM should shut down gracefully. In this case, that means waiting for all running transactions to terminate, and cleaning up its files, so that the next time it starts up, it does not attempt to recover its state. The dieNow() method makes the RM call System.exit() immediately, to simulate a system failure, leaving the database in its current state. When the RM next starts up, it will have to recover to a consistent state. Instead of killing the RM immediately, the dieBeforePointerSwitch() and dieAfterPointerSwitch() methods set flags so that the RM will call System.exit() in the middle of the next commit operation. See the API for details.

Part 2: Distributed Transactions (DUE: end of term at final exam time )
In the second part of the project, we will distribute the data tables among several RMs and perform distributed transactions across them.

The WorkflowController will be a front-end so that the eventual location (partitioning) of data on the RM's is not exposed to the client. To do this, the WC will support the same interface as the RM, and also the new function:

boolean reserveItinerary(int xid, String custName, List flightNumList, String location, boolean needCar, boolean needRoom);

This itinerary reservation is the sort of high level operation associated with a workflow controller. The parameters are: xid, transaction id; custName, name of customer; flightNumList, a List (e.g., a Vector) of String flight numbers; location, the place where a room or car might be reserved; and needCar/needRoom, true if the customer wants a car/room reservation.

The TransactionManager supports mainly the following operations: start, commit, abort, enlist. The transaction manager is present to coordinate the distributed transactions taking place across the several RMs involved. The interface of the TM is to be used as follows: whenever a request is made to an RM, it calls the TM's enlist method to tell the TM that it is involved in a transaction. The TM then keeps track of which RMs are involved in which transactions. The WC forwards a start/commit/abort call by the client directly to the TM. All other calls by the client are forwarded to the appropriate RM.

Since the TM & the RMs exist behind the WC interface, their interfaces don't have functions directly accesible to the client. On startup, the RMs will obtain a reference to the TM (through a RMI "Naming.lookup"). The WC connects to both TM and all the RMs on startup. After a RM enlists with a TM, the TM will also have a reference to that RM.

Here's a list of suggested steps to tackle Part 2:

  • Step 1. Setup:
    1. Download the tarball from here
    2. Install the tarball and compile as in Part 1. The code structure is very similar to Part 1. Don't forget to change RMIREGPORT in the Makefile.
    3. To run the system, start the components under the project/transaction directory in the following order (you'll need to open a few shell windows, or run some of the processes in the background):
      make runregistry &
      make runtm &
      make runrmflights &
      make runrmrooms &
      make runrmcars &
      make runrmcustomers &
      make runwc &
      make runclient &

  • Step 2. Integrate your RM code from Part 1 into the ResourceManager.java here. You should be able to reuse a lot of your code from Part 1. Then modify the WC to forward all calls to one specific RM.

  • Step 3. Run multiple RMs. Separate flights, cars, rooms and customers/reservations data each into one RM. The TM will maintain a list for each active transaction of, which RMs are involved. The WC will decide which data requests / transactions go where. On a commit/abort request (which is forwarded to the TM), the TM calls the appropriate functions on all RMs involved in the transaction.

  • Step 4. Implement two phase commit. At this stage, code the basics of two phase commit without worrying about failures. That is, don't worry yet about when things need to be written to disk.

  • Step 5. Now worry about what happens on failure. In particular, handle cases where components die in the middle of commit. For example, make sure that a RM can recover if it died before knowing the outcome of the transaction.

At this point, you will have a fully functioning distributed travel reservation application.

How to Submit

Make sure:
  • You have not modified WorkflowController.java or the LockManager code.
  • All your code is confined to the project/transaction directory (and possible subdirectories).
  • Your server puts all files in a directory called data. If the directory does not already exist, your server creates it.
  • You're on an ICS SUN machine.
  • You're using the correct version of java: version 1.2.2 in /usr/bin.
  • In project/transaction, after a "make clean", your server compiles without errors with "make server".
  • You can compile Client.java with "make client".
  • You can start components of the system with the commands given in the original Makefile distributed to you. For example
    java -classpath .. -DrmiPort=1234 -DrmiName=RMFlights -Djava.security.policy=./security-policy transaction.ResourceManagerImpl
    starts the RM for flights, where 1234 is the port of the running rmiregistry.
  • You create a file named README. Use the same requirements of the file as part 1.
Steps to submit:
  1. On an ICS SUN machine, cd to your project/transaction directory.
  2. Make sure you "make clean" to get rid of all .class files.
  3. tar cvf - * | gzip > ../ics214b-part1.tar.gz (for part 1) which will tar all the files under the current directory (including subdirectories) into a tar file, then compress it into a zipped file.
  4. Please make sure to submit only one copy before each due date.
Notes
  • You should use the java installation in /usr/bin/ (version 1.2.2) Try to add module load j2sdk java-jdk/1.2.2 the end of your .cshrc file. If you do "java -version", you should see the following:
    java version "1.2.2"
    Solaris VM (build Solaris_JDK_1.2.2_07a, native threads, sunwjit)
  • Put all files that your RM creates (database files, master pointer files, transaction lists...) in a directory called "data" in the project/transaction/ directory. This way one can simply do a "rm -rf data" to remove the entire database state. When your RM starts up, it will 1)check if "data" directory exists; if not, create one; 2) check if the relevant files are there; if not, create an empty database; if yes, try to "recover" from those files.
  • Let's assume that we do not expect more than one transaction to frequently be working on reversations for the same customer at the same time. So, assuming you take our suggestion and merge the RESERVATIONS table with the CUSTOMERS table, it will be acceptable if, instead of locking individual reservation rows, you decide to lock the entire customer row. However, again, it is never acceptable to do table-level locking.
  • The "make runserver" command attempts to start the rmiregistry at your unique port number. However, if you already have a previous instance of your rmiregistry running, you'll get a "...Port already in use..." exception. This is not a problem since the server (RM) should still work. Or, you can always use "ps -ef|grep $USER|grep rmiregistry" to find your old rmiregistry and kill it. For Part 2, this is no longer an issue because the registry is started explicitly with "make runregistry".
Acknowledgements
This project is based on a similar course project developed by Anand Rajaraman at Stanford, which was based on a similar project developed by Phil Bernstein at the University of Washington.