ArjunaCore exploits object-oriented techniques to present programmers with
a toolkit of Java classes from which application classes can inherit to obtain
desired properties, such as persistence and concurrency control. These classes
form a hierarchy, part of which is shown below.
Figure 1 - ArjunaCore class hierarchy.
Apart from specifying the scopes of transactions, and setting appropriate locks
within objects, the application programmer does not have any other responsibilities:
ArjunaCore and Transactional Objects for Java (TXOJ) guarantee that transactional
objects will be registered with, and be driven by, the appropriate transactions,
and crash recovery mechanisms are invoked automatically in the event of failures.
Recovery and Persistency
Making an object persistent and recoverable means that we shall be able to
store its final state or to retrieve its initial state according to the final
status of a transaction even in the presence of failures. ArjunaCore provides
a set of techniques to save to and to retrieve from the Object Store states
of objects. All objects made persistent with these ArjunaCore mechanisms are
assigned unique identifiers (instances of the Uid class), when they are created,
and this is to identify them within the object store. Due to common functionality
for persistency and recovery required by several applications, objects are stored
and retrieved from the object store using the same mechanism: the classes OutputObjectState
and InputObjecState.
At the root of the class hierarchy, given in Figure 1, is the class StateManager.
This class is responsible for object activation and deactivation and object
recovery. The simplified signature of the class is:
public abstract class StateManager
{
public boolean activate ();
public boolean deactivate (boolean commit);
public Uid get_uid (); // object’s identifier.
// methods to be provided by a derived class
public boolean restore_state (InputObjectState os);
public boolean save_state (OutputObjectState os);
protected StateManager ();
protected StateManager (Uid id);
};
Objects are assumed to be of three possible flavours. They may simply be recoverable,
in which case StateManager will attempt to generate and maintain appropriate
recovery information for the object. Such objects have lifetimes that do not
exceed the application program that creates them. Objects may be recoverable
and persistent, in which case the lifetime of the object is assumed to be greater
than that of the creating or accessing application, so that in addition to maintaining
recovery information StateManager will attempt to automatically load (unload)
any existing persistent state for the object by calling the activate (deactivate)
operation at appropriate times. Finally, objects may possess none of these capabilities,
in which case no recovery information is ever kept nor is object activation/deactivation
ever automatically attempted.
According to the its activation or deactivation a transactional object for
Java move from a passive state to an active state and vice-versa. The fundamental
life cycle of a persistent object in TXOJ is shown in Figure 2.
Figure 2 - The life cycle of a persistent object.
- The object is initially passive, and is stored in the object store as an
instance of the class OutputObjectState.
- When required by an application the object is automatically activated by
reading it from the store using a read_committed operation and is then converted
from an InputObjectState instance into a fully-fledged object by the restore_state
operation of the object.
- When the application has finished with the object it is deactivated by
converting it back into an OutputObjectState instance using the save_state
operation, and is then stored back into the object store as a shadow copy
using write_uncommitted. This shadow copy can be committed, overwriting the
previous version, using the commit_state operation. The existence of shadow
copies is normally hidden from the programmer by the transaction system. Object
de-activation normally only occurs when the top-level transaction within which
the object was activated commits.
While deactivating and activating a transactional object for java, the operations
save_state and restore_state are respectively invoked. These operations must
be implemented by the programmer since StateManager cannot detect user level
state changes. This gives the programmer the ability to decide which parts of
an object’s state should be made persistent. For example, for a spreadsheet
it may not be necessary to save all entries if some values can simply be recomputed.
The save_state implementation for a class Example that has two integer member
variables called A and B and one String member variable called C could simply
be:
public boolean save_state(OutputObjectState o)
{
if (!super.save_state(o))
return false;
try
{
o.packInt(A);
o.packInt(B);
o.packString(C));
}
catch (Exception e)
{
return false;
}
return true;
}
while, the corresponding restore_state implementation allowing to retrieve
similar values is:
public boolean restore_state(InputObjectState o)
{
if (!super.restore_state(o))
return false;
try
{
A = o.unpackInt();
B = o.unpackInt();
S = o.unpackString());
}
catch (Exception e)
{
return false;
}
return true;
}
Classes OutputObjectState and InputObjectState provide respectively operations
to pack and unpack instances of standard Java data types. In other words for
a standard Java data type, for instance Long or Short, there are corresponding
methods to pack and unpack, i.e., packLong or packShort and unpackLong or unpackShort.
Note: it is necessary for all save_state and restore_state
methods to call super.save_state and super.restore_state. This is to cater for
improvements in the crash recovery mechanisms.
The concurrency controller
The concurrency controller is implemented by the class LockManager which provides
sensible default behaviour while allowing the programmer to override it if deemed
necessary by the particular semantics of the class being programmed. The primary
programmer interface to the concurrency controller is via the setlock operation.
By default, the runtime system enforces strict two-phase locking following a
multiple reader, single writer policy on a per object basis. However, as shown
in Figure 1, by inheriting from the Lock class it is possible for programmers
to provide their own lock implementations with different lock conflict rules
to enable type specific concurrency control.
Lock acquisition is (of necessity) under programmer control, since just as
StateManager cannot determine if an operation modifies an object, LockManager
cannot determine if an operation requires a read or write lock. Lock release,
however, is under control of the system and requires no further intervention
by the programmer. This ensures that the two-phase property can be correctly
maintained.
public abstract class LockManager extends StateManager
{
public LockResult setlock (Lock toSet, int retry, int timeout);
};
The LockManager class is primarily responsible for managing requests to set
a lock on an object or to release a lock as appropriate. However, since it is
derived from StateManager, it can also control when some of the inherited facilities
are invoked. For example, LockManager assumes that the setting of a write lock
implies that the invoking operation must be about to modify the object. This
may in turn cause recovery information to be saved if the object is recoverable.
In a similar fashion, successful lock acquisition causes activate to be invoked.
The code below shows how we may try to obtain a write lock on an object:
public class Example extends LockManager
{
public boolean foobar ()
{
AtomicAction A = new AtomicAction;
/*
* The ArjunaCore AtomicAction class is here used to create
* a transaction. Any interface provided by the JTA or
* JTS interfaces that allow to create transactions can
* be used in association with the Locking mechanisms
* described in this trail.
*/
boolean result = false;
A.begin();
if (setlock(new Lock(LockMode.WRITE), 0) == Lock.GRANTED)
{
/*
* Do some work, and TXOJ will
* guarantee ACID properties.
*/
// automatically aborts if fails
if (A.commit() == AtomicAction.COMMITTED)
{
result = true;
}
}
else
A.rollback();
return result;
}
}
Further Reading
More details on Transactional Object For Java can be found in the ArjunaCore
Programming Guide.