JBoss.orgCommunity Documentation
Development Phases of a TxCore Application
First, develop new classes with characteristics like persistence, recoverability, and concurrency control.
Then develop the applications that make use of the new classes of objects.
Although these two phases may be performed in parallel and by a single person, this guide refers to the first step
as the job of the class developer, and the second as the job of the applications developer. The
class developer defines appropriate save_state
and restore_state
operations for the class, sets appropriate locks in operations, and invokes the appropriate TxCore class
constructors. The applications developer defines the general structure of the application, particularly with regard
to the use of atomic actions.
This chapter outlines a simple application, a simple FIFO Queue class for integer values. The Queue is implemented with a doubly linked list structure, and is implemented as a single object. This example is used throughout the rest of this manual to illustrate the various mechanisms provided by TxCore. Although this is an unrealistic example application, it illustrates all of the TxCore modifications without requiring in depth knowledge of the application code.
The application is assumed not to be distributed. To allow for distribution, context information must be propagated either implicitly or explicitly.
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 values of elements in the queue can also be changed or inspected. In this
example implementation, an array represents the queue. A limit of QUEUE_SIZE
elements has been imposed
for this example.
Example 6.1. Java interface definition of class queue
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;
};
Using an existing persistent object requires the use of a special constructor that takes the Uid of the persistent object, as shown in Example 6.2, “Class TransactionalQueue”.
Example 6.2. Class TransactionalQueue
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();
}
Example 6.3. Method save_state
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 6.4. Method restore_state
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 6.5. Method type
Because the Queue class is derived from the LockManager class, the operation type should be:
public String type ()
{
return "/StateManager/LockManager/TransactionalQueue";
}
If the operations of the queue
class are to be coded as atomic actions, then the enqueue
operation might have the structure given below. The dequeue
operation is similarly
structured, but is not implemented here.
Example 6.6. Method enqueue
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();
}
}
Example 6.7. Method queueSize
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;
}
The setValue
method is not implemented here, but is similar in structure to Example 6.8, “Method inspectValue”.
Example 6.8. Method inspectValue
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;
}
Rather than show all of the code for the client, this example concentrates on a representative portion. Before invoking operations on the object, the client must first bind to the object. In the local case this simply requires the client to create an instance of the object.
Example 6.9. Binding to the Object
public static void main (String[] args)
{
TransactionalQueue myQueue = new TransactionalQueue();
Before invoking one of the queues operations, the client starts a transaction. The queueSize operation is shown below:
AtomicAction A = new AtomicAction();
int size = 0;
try
{
A.begin(0);
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!);
}
}
Since the queue
object is persistent, the state of the object survives any failures of
the node on which it is located. The state of the object that survives is the state produced by the last top-level
committed atomic action performed on the object. If an application intends to perform two
enqueue
operations atomically, for example, you can nest the
enqueue
operations in another enclosing atomic action. In addition, concurrent operations
on such a persistent object are serialized, 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 are executed serially, even though logically
they could be executed concurrently. An example of this is modifying the states of two different elements in the
queue. The platform Development Guide addresses some of these issues.