|
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:
- Download the tarball from here
- Put the tarball in a directory you've created for this class, say, ~/ics214b
- cd ~/ics214b
- tar xvf part1.tar
- cd project/lockmgr
- make
- cd ../transaction
- 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.
- make
- 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.
- Then, open another shell window on the same machine, go to the transaction directory, and do: make runclient
- 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.
|
|
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:
- Download the tarball from here
- 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.
- 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:
- On an ICS SUN machine, cd to your project/transaction directory.
- Make sure you "make clean" to get rid of all .class files.
- 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.
- Please make sure to submit only one copy before each due date.
|