A transaction refers to the execution of a sequence of statements that operate on persistent data as a logical unit; that is, the operations performed by the statements individually are performed all together or not at all. This chapter describes the basic properties and types of transactions and explains how to write transactions using the ObjectStore API.
Overview of Transactions
Before a program can access persistent data, it must start a transaction. A transaction is the portion of a program whose changes to persistent data are atomic; that is, either all the changes are recorded or none of them. You mark the beginning and end of transaction using either macros or function calls that are provided by the ObjectStore API.
While a transaction is in progress, the program's actions can include reads and writes to persistent objects. The program can either commit or abort the transaction at any time.
Lexical and Dynamic Transactions
ObjectStore supports two types of transactions:
Transaction Commit and Abort
Transactions can terminate successfully or unsuccessfully. When they terminate successfully, they commit. All changes to persistent memory are made permanent and visible to other processes. A transaction is not considered to have terminated successfully until all its changes are recorded safely on stable storage. After a transaction commits, failures such as server crashes or network failures cannot erase the transaction's changes.
From the point of view of other processes, the ability to roll back a transaction to its pretransaction state means that the code within a transaction executes either all at once or not at all. That is, other processes do not see the intermediate results.
For information about aborting a transaction explicitly in order to perform a rollback, see Rolling Back to Persistent State.
Concurrency Control
An important function of a transaction is its support for concurrent database access. This support prevents one process's updates from interfering with another process's reads or updates. Concurrency control prevents this interference by ensuring that transactions have the following properties:
Marking Off a Transaction
When marking off transaction boundaries in your code, you must balance two opposing considerations:
OS_BEGIN_TXN(identifier,exception**,transaction-type) {
} OS_END_TXN(identifier)
The arguments to the macros are described in the following paragraphs.
exception** specifies a location in which ObjectStore stores 0 at the beginning of a transaction. If the transaction aborts, exception** points to the exception. To access the exception, you must declare a pointer of type tix_exception and supply its address as the argument. If you do not use the exception** argument, specify 0.
The following code example illustrates the exception** argument to the OS_BEGIN_TXN macro:
tix_exception* abort_reason_exception;
OS_BEGIN_TXN(txn1, &abort_reason_exception,
os_transaction::update){
After the transaction has terminated, you can examine abort_reason_exception to find out whether the transaction aborted. If the pointer is nonzero after the transaction's scope is exited, an exception occurred. See Explicit Aborts and Lexical Transactions for more information.
Transaction types: update and read-only
transaction-type is one of the following enumerators:
Note
If a deadlock occurs in a dynamic transaction, the transaction is not retried automatically and the deadlock must be handled by the programmer. For more information, see Using Dynamic Transactions.
Example of a Lexical Transaction
The following program uses a lexical transaction to write objects to a persistently stored database. The program prompts the user for a note and a priority number, then stores both in the database that the user has specified on the command line. Objects are stored in a linked list, the most recent object becoming the new head of the list, that is, the entry-point object.
// main.cc: Note program - main file
// Adds a note and a priority number to specified database
#include "note.hh"
note* head; // head of linked-list of notes
main(int argc, char** argv)
{
char buff[BUFSIZE], buff2[BUFSIZE];
int note_priority;
OS_ESTABLISH_FAULT_HANDLER {
if(argc!=2) { // check for name of database
cout << "Usage: " << argv[0] << " <database>" << endl;
return 1;
}
objectstore::initialize();
// open database; create one if it doesn't exist
os_database *db = os_database::open(argv[1], 0, 0644);
OS_BEGIN_TXN(t1,0,os_transaction::update) {
os_database_root *root_head = db->find_root("head");
if (!root_head) // if there's no root, create one
root_head = db->create_root("head");
head =
(note *)root_head->get_value(note::get_os_typespec());
cout << "Enter a note: " << flush; // Prompt for new note
cin.getline(buff, sizeof(buff));
cout << "Enter note priority: " << flush; // Prompt for priority
cin.getline(buff2, sizeof(buff2));
note_priority = atoi(buff2);
// allocate note object and make it the new entry point
head = new(db, note::get_os_typespec())
note(buff, head, note_priority);
root_head->set_value(head, note::get_os_typespec());
} OS_END_TXN(t1)
db->close(); // close database
} OS_END_FAULT_HANDLER
return 0;
}
OS_BEGIN_TXN(arg-list) { // outer transaction
OS_BEGIN_TXN(arg-list) { // inner transaction
// transaction block
} OS_END_TXN(arg) // end of inner transaction
} OS_END_TXN(arg) // end of outer transaction
One reason to nest transactions is to allow temporary writes to persistent memory that you want to discard at the end of the transaction. This type of nested transaction is an abort-only transaction. To set up an abort-only transaction, nest an update transaction inside a read-only transaction. The following example of an abort-only transaction replaces the transaction portion of the code in the program listed in Example of a Lexical Transaction. The statements in the inner transaction are the same as those in the original program, except for the addition of os_transaction::abort(), which rolls back the inner transaction. (See os_transaction::abort() in the C++ API Reference.) The outer transaction provides a read-only context for the inner transaction that avoids having to wait for a write lock.
// a nested, abort-only transaction
OS_BEGIN_TXN(txn1,0,os_transaction::read_only) {
OS_BEGIN_TXN(txn2,0,os_transaction::update) {
os_database_root *root_head = db->find_root("head");
if(!root_head)
root_head = db->create_root("head");
head =
(note *)root_head->get_value(note::get_os_typespec());
cout << "Enter a new note: " << flush;
cin.getline(buff, sizeof(buff));
cout << "Enter a note priority: " << flush;
cin.getline(buff2, sizeof(buff2));
note_priority = atoi(buff2);
head = new(db, note::get_os_typespec())
note(buff, head, note_priority);
root_head->set_value(head, note::get_os_typespec());
// roll back the transaction
os_transaction::abort();
} OS_END_TXN(txn2)
} OS_END_TXN(txn1)
static os_transaction* os_transaction::begin(
os_int32 transaction_type = os_transaction::update
);
static void os_transaction::commit(
os_transaction* txn = os_transaction::get_current());
The statements executed between begin() and commit() are all considered within the same transaction. The following sections describe how to use these functions. For reference information about begin() and commit(), see the following in the C++ API Reference:
begin() takes one argument, transaction_type, which specifies the transaction type and can be one of the following enumerators:
// start of transaction
os_transaction::begin(os_transaction::update);
os_database_root *root_head = db->find_root("head");
if(!root_head) // if there's no root, create one
root_head = db->create_root("head");
head = (note *)root_head->get_value(note::get_os_typespec());
loop:
cout << "Enter a new note: " << flush; // Prompt for new note
cin.getline(buff, sizeof(buff));
cout << "Enter a note priority: " << flush; // Prompt for priority
cin.getline(buff2, sizeof(buff2));
note_priority = atoi(buff2);
// allocate note object and make it the new entry point
head = new(db, note::get_os_typespec())
note(buff, head, note_priority);
root_head->set_value(head, note::get_os_typespec());
cout << "Do you want to commit, write another note, or abort";
cout << " (c/w/a)? " << flush;
cin.getline(buff, sizeof(buff));
if (buff[0] == 'c')
os_transaction::commit(); // commit transaction
else if (buff[0] == 'a')
os_transaction::abort(); // abort transaction
else
goto loop; // get another note
// end of transaction
Waiting for Locks
Generally speaking, when you access data in the database, you are given exclusive access to that data for the duration of the transaction in which the access takes place; that is, when you access data, that data is locked. The data is not unlocked until the end of the transaction. Read Locks and Write Locks
Locking differs depending on whether you are reading or writing data. When a session reads persistent data, the page on which the data resides is read locked; other sessions cannot write to that page. They can read it, but writing is delayed until the outermost transaction of the locking session completes and the lock is released. When a session writes to persistent data, the page on which the data resides is write locked; other sessions are prevented from reading or writing to that page. Aborts and Locks
If an abort occurs during a transaction, the lock is released. If the transaction is nested, all locks acquired during the nested transaction are released, all changes in the nested transaction are rolled back, and the database is restored to its state before the outermost transaction. Lock Timeouts
You can set a timeout for read- and write-lock attempts by calling objectstore::set_lock_timeout() to limit the amount of time your application waits to acquire a lock. To retrieve the current timeout value, call objectstore::get_lock_timeout(). When the timeout is exceeded, an exception is signaled. Handling the exception allows you to continue with alternative processing and to make a later attempt to acquire the lock.
Lock Probes
To determine whether a specified address is read locked, write locked, or unlocked, see objectstore::get_lock_status() in the C++ API Reference.
Explicit Lock Acquisition
Normally, ObjectStore performs locking automatically and transparently to the user. You can, however, explicitly lock a specified page range for reading or writing; see objectstore::acquire_lock() in the C++ API Reference.
Rolling Back to Persistent State
You can force a roll back by calling one of the following:
static os_transaction::abort(
os_transaction* txn = os_transaction::get_current());
static void os_transaction::abort_top_level();
Calling either of these functions causes an explicit abort. The following sectionsdescribe how to use these functions to cause an explicit abort and how to check for an explicit abort in a lexical transaction. For reference information about abort() and abort_top_level(), see the following in the C++ API Reference:
When an explicit abort occurs in a dynamic transaction, program control passes to the statement immediately following the abort(). In a lexical transaction, program control passes to the statement immediately following the current transaction block. In either case, changes to persistent data are rolled back to the state as of the beginning of the transaction. All locks acquired during the transaction are released, including when the explicit abort occurs in a nested transaction.
For example programs that call os_transaction::abort() to cause an explicit abort, see the following sections:
For reference information about get_current() and get_parent(), see the following in the C++ API Reference:
The following example aborts a transaction one level up from the current transaction:
os_transaction *child_txn, *parent_txn;
if (child_txn = os_transaction::get_current())
if (parent_txn = child_txn->get_parent())
os_transaction::abort(parent_txn);
In general, an exception that aborts a transaction also aborts the program. To examine the pointer argument to the OS_BEGIN_TXN() macro, you must enclose the entire transaction in a TIX exception handler; see Establishing a TIX Exception Handler.
However, an explicit-abort exception, err_explicit_abort, aborts only the transaction, not the program. This means that you can handle an explicit abort without establishing a TIX exception handler. To determine whether an explicit abort did occur, check the tix_exception pointer argument for nonzero. If it is 0, an exception did not occur. If it is nonzero, compare the pointer to the global address of err_explicit_abort and handle it accordingly.
tix_exception* my_exception;
OS_BEGIN_TXN(txn1,&my_exception,os_transaction::update) {
// transaction block
} OS_END_TXN(txn1)
if (my_exception && (my_exception == &err_explicit_abort))
cout << "Transaction was explicitly aborted" << endl;
For reference information about tix_exception, see tix_exception in the C++ API Reference.
Local and Global Transactions
For applications that use multiple threads in a single session, transactions can be either global or local. Lexical transactions - that is, transactions that begin with the OS_BEGIN_TXN() macro - are always local. Dynamic transactions - that is, transactions that begin with a call to os_transaction::begin() - are local by default, but can be made global by specifying the os_transaction::global enumerator in the call to os_transaction::begin().
os_transaction::begin(os_transaction::update, os_transaction::global);A thread enters a local transaction by calling os_transaction::begin() or OS_BEGIN_TXN(). A thread enters a global transaction when it calls os_transaction::begin() and specifies os_transaction::global, or when another thread of the same session calls os_transaction::begin() and specifies os_transaction::global. When one thread enters a global transaction, all other threads in the same session automatically enter the same transaction.
Two threads cannot be in a local transaction at the same time. Local transactions synchronize access to the ObjectStore run time by serializing the transactions of the different threads - that is, by making the transactions run one after another without overlapping. After one thread starts a local transaction, if another thread attempts to start a transaction or enter the ObjectStore run time, it is blocked by a mutex lock until the local transaction completes.
Global transactions allow for a somewhat higher degree of concurrency. After one thread enters the ObjectStore run time, if another thread attempts to enter the ObjectStore run time, it is blocked until control in the first thread exits from the run time.
To determine whether a transaction is local or global, call os_transaction::get_scope(), as described in os_transaction::get_scope() in the C++ API Reference. For information about os_transaction::begin(), see os_transaction::begin() in the C++ API Reference. For information about using threads with ObjectStore, see Chapter 3, Multithread and Multisession Applications, in the Advanced C++ API User Guide.
Two-Phase Commit
Two-phase commit is the process by which transactions that use multiple Servers commit in a consistent manner. The majority of transactions do not use multiple Servers and are unaffected by two-phase commit.
recovaux.C:1123(-2): This server is a participant in a multi server transaction recovaux.C:1127(-2): (id: 8 global id: 67372226,649525822,20971521) recovaux.C:1138(-2): The coordinator of the transaction is: recovaux.C:1143(-2): peachbottom recovaux.C:1147(-2): There are 2 blocks involved in the transaction on this server. Server startedThe message indicates that during the restart process, the Server noticed that a multi-Server transaction was in the process of committing when the host system crashed. The information about transaction IDs can be used to match outstanding transactions with other Servers that also might be recovering. The message about the coordinator indicates that it receives the decision from that host (in this case, peachbottom) when that Server recovers.
During peachbottom's restart process, it too notices that a multi-Server transaction was in the commit process when the host crashed, and prints a similar message:
recovaux.C:1120(-2): This server is coordinator for a multi server transaction recovaux.C:1127(-2): (id: 6 global id: 67372226,649525822,20971521) recovaux.C:1130(-2): The transaction committed. recovaux.C:1135(-2): The 2 participant servers in the transaction are: recovaux.C:1143(-2): peachbottom recovaux.C:1143(-2): seabrook recovaux.C:1147(-2): There are 0 blocks involved in the transaction on this server. Server startedNote that the message identifies the participant Servers, including the sender.
peachbottom then notifies seabrook of the decision for the transaction (committed), at which time seabrook prints the message
recovaux.C:1234(-2): Transaction committed recovaux.C:1252(-2): (id=8 global id: 67372226,649525822,20971521)The recovery is now complete for this transaction.
The pages involved in this transaction are in limbo during the commit phases and the restart processing. However, during two-phase commit restart processing, it is possible for other requests to the Servers to be processed after they are started up. If one of those requests is for a page that is being recovered as part of a multi-Server transaction that was committing, the client program receives an error indicating that the block is not recovered. This error is defined as
DEFINE_EXCEPTION(err_svr_blknotrecovered,
"The block is blocked by an incomplete two phase commit",
&err_svr);
After the recovery processing is complete for the corresponding two-phase commit, the block becomes free to use again. (For information about the DEFINE_EXCEPTION() macro, see User-Defined Exceptions. )For information about a special form of concurrency control, multiversion concurrency control (MVCC), using special techniques of delaying propagation and intentionally aborting transactions, see Multiversion Concurrency Control (MVCC) in Chapter 2 of the Advanced C++ API User Guide.
Updated: 03/09/99 13:54:47