Chapter 14. Constructing an Application Using Transactional Objects for Java

14.1. Application Construction

Developing a JBoss Transaction Service application involves two distinct phases. First, the class developer writes new classes which need to be persistent, recoverable, or concurrency-controlled. Then, the application developer uses the classes you've created in your application. These developers may be the same person, but the two different roles imply different concerns. The class developer needs to focus on developing appropriate save_state and restore_state methods, as well as setting appropriate locks in operations and invoking the appropriate JBoss Transaction Service class constructors. The application developer's concern is defining the general structure of the application, particularly with regard to the use of atomic actions, or transactions.
This chapter outlines a simple application, a simple FIFO Queue class for integer values. The Queue uses a double linked list structure, and is implemented as a single object. The example is used throughout the remainder of this manual, to illustrate the various mechanisms provided by JBoss Transaction Service. Although the example is simplistic, it shows all possible modifications to JBoss Transaction Service without requiring in-depth knowledge of the application code.
Examples in this chapter assume that the application is not distributed. In a distributed application, context information must be propagated either implicitly or explicitly.

14.1.1. 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 into the queue (enqueue) and to be removed from it (dequeue), as well as the ability 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.

Example 14.1. Java Interface Definition of the Que Class

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

14.1.2. Constructors and deconstructors

Example 14.2. Using an Existing Persistent Object

To use an existing persistent object, you need to use a special constructor that is required to take the Uid of the persistent object.
	  public TransactionalQueue (Uid u)
	  {
	  super(u);
	  
	  numberOfElements = 0;
	  }

Example 14.3. Creating a New Persistent Object

	  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);
	  }
	  }
To use an atomic action within the constructor for a new object, follow the guidelines outlined earlier, which ensure that the state of the object is written to the object store when the appropriate top-level atomic action commits. To use atomic actions in a constructor, you need to first declare the action and invoke its begin method. Then, the operation must set an appropriate lock on the object. Afterward, the main body of the constructor is executed. If successful, the atomic action is committed. Otherwise, it is aborted.
The destructor of the queue class only needs to call the terminate method of the LockManager method.

Example 14.4. Destructor of the Queue Class

	  public void finalize ()
	  {
	  super.terminate();
	  }

14.1.3. The save_state, restore_state, and type Methods

Example 14.5. The save_state Method

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

Example 14.6. The restore_state Method

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

Example 14.7. The type Method

Because the Queue class is derived from the LockManager class, the operation type should return a transactional queue.
	  public String type ()
	  {
	  return "/StateManager/LockManager/TransactionalQueue";
	  }

14.1.4. enqueue/dequeue operations

If the operations of the queue class will be atomic actions, then the enqueue operation in Example 14.8, “The enqueue Method” is appropriate as a guideline. The dequeue would have a similar structure, but is not included as an example.

Example 14.8. The enqueue Method

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

14.1.5. The queueSize Method

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

14.1.6. The inspectValue and setValue Methods

Note

The implementation of the setValue is not shown, but it can be inferred from the inspectValue method which is 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;
	}

14.1.7. The Client

The example code for the client only includes a representative portion, rather than the full code. Before invoking operations on the object, the client needs to bind to it. If the client is run locally, it only needs to create an instance of the object.

Example 14.9. Creating an Instance of the TransactionalQueue Object

	  public static void main (String[] args)
	  {
	  TransactionalQueue myQueue = new TransactionalQueue();
Before invoking one of the queue’s operations, the client starts a transaction, using the queueSize method.

Example 14.10. The queueSize Method

	  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!”);
	  }

14.1.8. Notes

Because the queue object is persistent, the state of the object will survive any failures of the node on which it is located. The preserved state will be the one produced by the last top-level committed atomic action performed on the object. If the application needs to perform two enqueue operations atomically, you can nest the enqueue operations inside another enclosing atomic action. In addition, concurrent operations on a persistent object are serialized, preventing inconsistencies in the state of the object. Be aware that since the elements of the queue objects are not individually concurrency controlled, certain combinations of concurrent operation invocations are executed serially, when logically they could be executed concurrently. This happens when modifying the states of two different elements in the queue.