Chapter 6. Constructing a Transactional objects for Java application

6.1. Application construction

There are two distinct phases to the development of a TxCore application:
  • Developing new classes with certain characteristics (for example, Persistent, Recoverable, Concurrency Controlled).
  • Developing the application(s) that make use of the new classes of objects.
Although these two phases may be performed in parallel and by a single person, we shall refer to the first step as the job of the class developer and the second as the job of the applications developer. The class developer will be concerned about defining appropriate save_state and restore_state operations for the class, setting appropriate locks in operations, and invoking the appropriate TxCore class constructors. The applications developer will be more concerned with defining the general structure of the application, particularly with regard to the use of atomic actions.
This chapter illustrates the points made in previous sections by outlining a simple application: in this case a simple FIFO Queue class for integer values will be developed. The implementation of the Queue will be with a doubly linked list structure, and it will be implemented as a single object. We shall be using this example throughout the rest of this manual to help illustrate the various mechanisms provided by TxCore. While this is an unrealistic example application it enables all of the TxCore modifications to be described without requiring in depth knowledge of the application code.
In the rest of this chapter we shall assume that the application is not distributed. If this is not the case, then context information must be propagated either implicitly or explicitly.
Queue description

The queue is a traditional FIFO queue, where elements are added to the front and removed from the back. The operations provided by the queue class allow the values to be placed on to the queue (enqueue) and to be removed from it (dequeue), and it is also possible to change or inspect the values of elements in the queue. In this example implementation, an array is used to represent the queue. A limit of QUEUE_SIZE elements has been imposed for this example.

The Java interface definition of this simple queue class is given below:
public class TransactionalQueue extends LockManager
{
	public TransactionalQueue (Uid uid);
	public TransactionalQueue ();
	public void finalize ();
	
	public void enqueue (int v) throws OverFlow, UnderFlow,
	QueueError, Conflict;
	public int dequeue  () throws OverFlow, UnderFlow,
	QueueError, Conflict;
	
	public int queueSize ();
	public int inspectValue (int i) throws OverFlow,
	UnderFlow, QueueError, Conflict;
	public void setValue (int i, int v) throws OverFlow,
	UnderFlow, QueueError, Conflict;
	
	public boolean save_state (OutputObjectState os, int ObjectType);
	public boolean restore_state (InputObjectState os, int ObjectType);
	public String type ();
	
	public static final int QUEUE_SIZE = 40; // maximum size of the queue
	
	private int[QUEUE_SIZE] elements;
	private int numberOfElements;
};
Constructors and deconstructors

As stated in the previous section, to use an existing persistent object requires the use of a special constructor that is required to take the Uid of the persistent object; the implementation of such a constructor is given below:

public TransactionalQueue (Uid u)
{
	super(u);
	
	numberOfElements = 0;
}
The constructor that creates a new persistent object is similar:
public TransactionalQueue ()
{
	super(ObjectType.ANDPERSISTENT);
	
	numberOfElements = 0;
	
	try
	{
		AtomicAction A = new AtomicAction();
	
		A.begin(0);	// Try to start atomic action
	
		// Try to set lock
	
			if (setlock(new Lock(LockMode.WRITE), 0) == LockResult.GRANTED)
			{
				A.commit(true);	// Commit
			}
			else 	// Lock refused so abort the atomic action
				A.rollback();
			}
	catch (Exception e)
	{
		System.err.println(“Object construction error: “+e);
		System.exit(1);
	}
}
The use of an atomic action within the constructor for a new object follows the guidelines outlined earlier and ensures that the object’s state will be written to the object store when the appropriate top level atomic action commits (which will either be the action A or some enclosing action active when the TransactionalQueue was constructed). The use of atomic actions in a constructor is simple: an action must first be declared and its begin operation invoked; the operation must then set an appropriate lock on the object (in this case a WRITE lock must be acquired), then the main body of the constructor is executed. If this is successful the atomic action can be committed, otherwise it is aborted.
The destructor of the queue class is only required to call the terminate operation of LockManager
public void finalize ()
{
	super.terminate();
}
save_state, resotre_state and type

The implementations of save_state and restore_state are relatively simple for this example:

public boolean save_state (OutputObjectState os, int ObjectType)
{
	if (!super.save_state(os, ObjectType))
	return false;
	
	try
	{
		os.packInt(numberOfElements);
	
		if (numberOfElements > 0)
		{
			for (int i = 0; i < numberOfElements; i++)
			    os.packInt(elements[i]);
		}
			    
		return true;
	}
	catch (IOException e)
	{
		return false;
	}
}

public boolean restore_state (InputObjectState os, int ObjectType)
{
	if (!super.restore_state(os, ObjectType))
		return false;
		    
	try
	{
		numberOfElements = os.unpackInt();
		    
		if (numberOfElements > 0)
		{
			for (int i = 0; i < numberOfElements; i++)
				    elements[i] = os.unpackInt();
		}
		    
		return true;
	}
	catch (IOException e)
	{
		return false;
	}
}
Because the Queue class is derived from the LockManager class, the operation type should be:
public String type ()
{
	return "/StateManager/LockManager/TransactionalQueue";
}
enqueue/dequeue operations

If the operations of the queue class are to be coded as atomic actions, then the enqueue operation could have the structure given below (the dequeue operation would be similarly structured):

public void enqueue (int v) throws OverFlow, UnderFlow, QueueError
{
	AtomicAction A = new AtomicAction();
	boolean res = false;
	
	try
	{
		A.begin(0);
	
		if (setlock(new Lock(LockMode.WRITE), 0) == LockResult.GRANTED)
		{
			if (numberOfElements < QUEUE_SIZE)
			       {
			       		elements[numberOfElements] = v;
			       		numberOfElements++;
			       		res = true;
			       }
			       else
			       {
			       		A.rollback();
			      		throw new UnderFlow();
			       }
		}
			       
		if (res)
			 A.commit(true);
		else
		{
			 A.rollback();
			       		throw new Conflict();
		}
	}
	catch (Exception e1)
	{
		throw new QueueError();
	}
}
queueSize

The implementation of queueSize is shown below:

public int queueSize () throws QueueError, Conflict
{
	AtomicAction A = new AtomicAction();
	int size = -1;
	
	try
	{
		A.begin(0);
	
		if (setlock(new Lock(LockMode.READ), 0) == LockResult.GRANTED)
			size = numberOfElements;
	
		if (size != -1)
			A.commit(true);
		else
		{
			A.rollback();
	
			throw new Conflict();
		}
	}
	catch (Exception e1)
	{
		throw new QueueError();
	}
	
	return size;
}
inspectValue/setValue operations

The implementation of inspectValue is shown below. setValue is similar, and not shown.

public int inspectValue (int index) throws UnderFlow,
	OverFlow, Conflict, QueueError
{
	AtomicAction A = new AtomicAction();
	boolean res = false;
	int val = -1;
	
	try
	{
		A.begin();
	
		if (setlock(new Lock(LockMode.READ), 0) == LockResult.GRANTED)
		{
			if (index < 0)
		   	 {
		   		 A.rollback();
		    		throw new UnderFlow();
		    	}
		else
		{
		    // array is 0 - numberOfElements -1
		    
		    if (index > numberOfElements -1)
		{
			A.rollback();
			throw new OverFlow();
		}
		else
		{
			val = elements[index];
			res = true;
		}
		}
		}
		
		if (res)
			A.commit(true);
		else
		{
			A.rollback();
			throw new Conflict();
		}
	}
	catch (Exception e1)
	{
		throw new QueueError();
	}
		
	return val;
}
The client

Rather than show all of the code for the client, we shall concentrate on a representative portion. Before invoking operations on the object, the client must obviously first bind to it. In the local case this simply requires the client to create an instance of the object.

public static void main (String[] args)
{
TransactionalQueue myQueue = new TransactionalQueue();
Before invoking one of the queue’s operations, the client starts a transaction. The queueSize operation is shown below:
AtomicAction A = new AtomicAction();
int size = 0;
	
try
{
	A.begin(0);
s
	try
	{
		size = queue.queueSize();
	}
	catch (Exception e)
	{
	}
	
	if (size >= 0)
	{
		A.commit(true);
	
		System.out.println(“Size of queue: “+size);
	}
	else
		A.rollback();
}
catch (Exception e)
{
	System.err.println(“Caught unexpected exception!”);
}
Comments

Since the queue object is persistent, then the state of the object will survive any failures of the node on which it is located. The state of the object that will survive is that produced by the last top-level committed atomic action performed on the object. If it is the intention of an application to perform two enqueue operations atomically, for example, then this can be done by nesting the enqueue operations in another enclosing atomic action. In addition, concurrent operations on such a persistent object will be serialised, thereby preventing inconsistencies in the state of the object. However, since the elements of the queue objects are not individually concurrency controlled, certain combinations of concurrent operation invocations will be executed serially, whereas logically they could be executed concurrently. For example, modifying the states of two different elements in the queue. In the next section we address some of these issues.