JBoss.orgCommunity Documentation
A transaction is a unit of work that encapsulates multiple database actions such that that either all the encapsulated actions fail or all succeed.
Transactions ensure data integrity when an application interacts with multiple datasources.
This chapter contains a description of the use of the TxCore transaction engine and the Transactional Objects for Java (TXOJ) classes and facilities. The classes mentioned in this chapter are the key to writing fault-tolerant applications using transactions. Thus, they are described and then applied in the construction of a simple application. The classes to be described in this chapter can be found in the com.arjuna.ats.txoj and com.arjuna.ats.arjuna packages.
Although JBoss Transaction Service can be embedded in various containers, such as JBoss Application Server, it remains a stand-alone transaction manager as well. There are no dependencies between the core JBoss Transaction Service and any container implementations.
In keeping with the object-oriented view, the mechanisms needed to construct reliable distributed applications are presented to programmers in an object-oriented manner. Some mechanisms need to be inherited, for example, concurrency control and state management. Other mechanisms, such as object storage and transactions, are implemented as TxCore objects that are created and manipulated like any other object.
When the manual talks about using persistence and concurrency control facilities it assumes that the Transactional Objects for Java (TXOJ) classes are being used. If this is not the case then the programmer is responsible for all of these issues.
TxCore 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 in Figure 2.1, “TxCore Class Hierarchy” and which will be described later in this document.
Apart from specifying the scopes of transactions, and setting appropriate locks within objects, the application programmer does not have any other responsibilities: TxCore and 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.
TxCore needs to be able to remember the state of an object for several purposes.
The state represents some past state of the object.
The state represents the final state of an object at application termination.
Since these requirements have common functionality they are all implemented using the same mechanism: the classes
InputObjectState
and OutputObjectState
. The classes maintain an
internal array into which instances of the standard types can be contiguously packed or unpacked using appropriate
pack
or unpack
operations. This buffer is automatically resized
as required should it have insufficient space. The instances are all stored in the buffer in a standard form
called network byte order, making them machine independent. Any other
architecture-independent format, such as XDR or ASN.1, can be implemented simply by replacing the operations with
ones appropriate to the encoding required.
Implementations of persistence can be affected by restrictions imposed by the Java SecurityManager. Therefore, the object store provided with TxCore is implemented using the techniques of interface and implementation. The current distribution includes implementations which write object states to the local file system or database, and remote implementations, where the interface uses a client stub (proxy) to remote services.
Persistent objects are assigned unique identifiers, which are instances of the Uid
class,
when they are created. These identifiers are used to identify them within the object store. States are read using
the read_committed
operation and written by the write_committed
and write_uncommitted
operations.
At the root of the class hierarchy is the class
StateManager
. StateManager
is responsible for object activation and
deactivation, as well as object recovery. Refer to Example 2.1, “Statemanager” for the simplified
signature of the class.
Example 2.1. Statemanager
public abstract class StateManager
{
public boolean activate ();
public boolean deactivate (boolean commit);
public Uid get_uid (); // objects 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 flavors.
Three Flavors of Objects
StateManager
attempts to generate and maintain appropriate recovery information for
the object. Such objects have lifetimes that do not exceed the application program that creates them.
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
attempts to
automatically load or unload any existing persistent state for the object by calling the
activate
or deactivate
operation at appropriate times.
No recovery information is ever kept, nor is object activation or deactivation ever automatically attempted.
If an object is recoverable or recoverable and persistent, then
StateManager
invokes the operations save_state
while performing
deactivate
, and restore_state
while performing
activate
,) at various points during the execution of the application. 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 integer member variables called A, B and C might be implemented as in Example 2.2, “save_state Implementation”.
Example 2.2. save_state
Implementation
public boolean save_state(OutputObjectState o)
{
if (!super.save_state(o))
return false;
try
{
o.packInt(A);
o.packInt(B);
o.packInt(C));
}
catch (Exception e)
{
return false;
}
return true;
}
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.
A persistent object not in use is assumed to be held in a passive state, with its state residing in an object store and activated on demand. The fundamental life cycle of a persistent object in TXOJ is shown in Figure 2.2, “Life cycle of a persistent Object in TXOJ”.
During its life time, a persistent object may be made active then passive many times.
The concurrency controller is implemented by the class LockManager
, which provides sensible
default behavior while allowing the programmer to override it if deemed necessary by the particular semantics of
the class being programmed. As with StateManager
and persistence, concurrency control
implementations are accessed through interfaces. As well as providing access to remote services, the current
implementations of concurrency control available to interfaces include:
Locks are made persistent by being written to the local file system or database.
Locks are maintained within the memory of the virtual machine which created them. This implementation has better performance than when writing locks to the local disk, but objects cannot be shared between virtual machines. Importantly, it is a basic Java object with no requirements which can be affected by the SecurityManager.
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 2.1, “TxCore Class Hierarchy”, by inheriting
from the Lock
class, you can provide your 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 class LockResult
{
public static final int GRANTED;
public static final int REFUSED;
public static final int RELEASED;
};
public class ConflictType
{
public static final int CONFLICT;
public static final int COMPATIBLE;
public static final int PRESENT;
};
public abstract class LockManager extends StateManager
{
public static final int defaultRetry;
public static final int defaultTimeout;
public static final int waitTotalTimeout;
public final synchronized boolean releaselock (Uid lockUid);
public final synchronized int setlock (Lock toSet);
public final synchronized int setlock (Lock toSet, int retry);
public final synchronized int setlock (Lock toSet, int retry, int sleepTime);
public void print (PrintStream strm);
public String type ();
public boolean save_state (OutputObjectState os, int ObjectType);
public boolean restore_state (InputObjectState os, int ObjectType);
protected LockManager ();
protected LockManager (int ot);
protected LockManager (int ot, int objectModel);
protected LockManager (Uid storeUid);
protected LockManager (Uid storeUid, int ot);
protected LockManager (Uid storeUid, int ot, int objectModel);
protected void terminate ();
};
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.
Example 2.3, “Example Class” shows how to try to obtain a write lock on an object.
Example 2.3. Example
Class
public class Example extends LockManager
{
public boolean foobar ()
{
AtomicAction A = new AtomicAction;
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;
}
}
The transaction protocol engine is represented by the AtomicAction
class, which uses
StateManager
to record sufficient information for crash recovery mechanisms to complete the
transaction in the event of failures. It has methods for starting and terminating the transaction, and, for those
situations where programmers need to implement their own resources, methods for registering them with the current
transaction. Because TxCore supports sub-transactions, if a transaction is begun within the scope of an already
executing transaction it will automatically be nested.
You can use TxCore with multi-threaded applications. Each thread within an application can share a transaction or execute within its own transaction. Therefore, all TxCore classes are also thread-safe.
Example 2.4. Relationships Between Activation, Termination, and Commitment
{
. . .
O1 objct1 = new objct1(Name-A);/* (i) bind to "old" persistent object A */
O2 objct2 = new objct2(); /* create a "new" persistent object */
OTS.current().begin(); /* (ii) start of atomic action */
objct1.op(...); /* (iii) object activation and invocations */
objct2.op(...);
. . .
OTS.current().commit(true); /* (iv) tx commits & objects deactivated */
} /* (v) */
This could involve the creation of stub objects and a call to remote objects. Here, we re-bind to an
existing persistent object identified by Name-A
, and a new persistent object. A
naming system for remote objects maintains the mapping between object names and locations and is described
in a later chapter.
As a part of a given invocation, the object implementation is responsible to ensure that it is locked in read or write mode, assuming no lock conflict, and initialized, if necessary, with the latest committed state from the object store. The first time a lock is acquired on an object within a transaction the object’s state is acquired, if possible, from the object store.
This includes updating of the state of any modified objects in the object store.
The principal classes which make up the class hierarchy of TxCore are depicted below.
StateManager
LockManager
User-Defined Classes
Lock
User-Defined Classes
AbstractRecord
RecoveryRecord
LockRecord
RecordList
Other management record types
AtomicAction
TopLevelTransaction
Input/OutputObjectBuffer
Input/OutputObjectState
ObjectStore
Programmers of fault-tolerant applications will be primarily concerned with the classes
LockManager
, Lock
, and AtomicAction
. Other
classes important to a programmer are Uid
and ObjectState
.
Most TxCore classes are derived from the base class StateManager
, which provides primitive
facilities necessary for managing persistent and recoverable objects. These facilities include support for the
activation and de-activation of objects, and state-based object recovery.
The class LockManager
uses the facilities of StateManager
and
Lock
to provide the concurrency control required for implementing the serializability
property of atomic actions. The concurrency control consists of two-phase locking in the current implementation.
The implementation of atomic action facilities is supported by AtomicAction
and
TopLevelTransaction
.
Consider a simple example. Assume that Example
is a user-defined persistent class suitably
derived from the LockManager
. An application containing an atomic transaction
Trans
accesses an object called O
of type Example,
by invoking the operation op1
, which involves state changes to
O
. The serializability property requires that a write lock must be acquired on
O
before it is modified. Therefore, the body of op1
should
contain a call to the setlock
operation of the concurrency controller.
Example 2.5. Simple Concurrency Control
public boolean op1 (...)
{
if (setlock (new Lock(LockMode.WRITE) == LockResult.GRANTED)
{
// actual state change operations follow
...
}
}
Procedure 2.1. Steps followed by the operation setlock
The operation setlock
, provided by the LockManager
class,
performs the following functions in Example 2.5, “Simple Concurrency Control”.
Check write lock compatibility with the currently held locks, and if allowed, continue.
Call the StateManager operation activate
.activate
will load,
if not done already, the latest persistent state of O
from the object store, then call
the StateManager
operation modified
, which has the effect of
creating an instance of either RecoveryRecord
or
PersistenceRecord
for O
, depending upon whether
O
was persistent or not. The Lock is a WRITE lock so the old state of the object must
be retained prior to modification. The record is then inserted into the RecordList of Trans.
Create and insert a LockRecord
instance in the RecordList
of
Trans
.
Now suppose that action Trans
is aborted sometime after the lock has been acquired. Then
the rollback
operation of AtomicAction
will process the
RecordList
instance associated with Trans
by invoking an
appropriate Abort
operation on the various records. The implementation of this operation
by the LockRecord
class will release the WRITE lock while that of
RecoveryRecord
or PersistenceRecord
will restore the prior state of
O
.
It is important to realize that all of the above work is automatically being performed by TxCore on behalf of the application programmer. The programmer need only start the transaction and set an appropriate lock; TxCore and TXOJ take care of participant registration, persistence, concurrency control and recovery.