C++ API User Guide

Chapter 5

Transactions

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.

The following topics are discussed:

Note
For information about more advanced topics concerning transactions, see Chapter 5, Transactions, of the Advanced C++ API User Guide.

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.

Access to persistent data must always take place within a transaction. If you attempt to access persistent data outside a transaction, err_no_trans is signaled. However, statements that create, destroy, open, or close a database can occur inside or outside a transaction; see Chapter 4, Basic Database Operations.

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:

Both types of transactions are described in Using Lexical Transactions and Using Dynamic 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.

When transactions terminate unsuccessfully, they abort; all changes are undone and rolled back to the pretransaction state.

A transaction abort can take several forms:

Rolling Back a Transaction

If the transaction in which changes were made is aborted, the changes are undone or rolled back to the pretransaction state. After an abort, your program sees persistent memory as it appeared just before the aborted transaction started.

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:

ObjectStore implements concurrency control using strict two-phase locking, as described in Locking.

Marking Off a Transaction

When marking off transaction boundaries in your code, you must balance two opposing considerations:

Dynamic transactions offer the most flexibility in setting up optimal boundaries for a transaction.

Using Lexical Transactions

To demarcate a lexical transaction, use the following macros:

OS_BEGIN_TXN(identifier,exception**,transaction-type) { 
} OS_END_TXN(identifier) 
Note
There must be no white space anywhere in the argument list. The enclosing braces are not required, but some source code editors might complain if you do not include them.

The arguments to the macros are described in the following paragraphs.

Arguments
identifier is a transaction tag. The tag must be be the same in both macros. Different transactions in the same function must use different tags. The tags are used to construct statement labels and therefore have the same scope as labels in C++.

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:

For reference information about the lexical-transaction macros, see the following in the C++ API Reference:

Deadlocks and Automatic Retries

When an abort occurs in a lexical transaction because of a deadlock, the transaction is retried automatically until it either completes successfully or the maximum number of retries is exhausted. By default, the maximum retry count for any lexical transaction in a given process is 10. You can retrieve and change the current retry count; see objectstore::get_transaction_max_retries() or objectstore::set_transaction_max_retries() in the C++ API Reference. If you change the retry count, the new count is effective for the current session only.

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.

Here is the program:

// 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; 
} 
Available on line
This example program is available on line in

Nesting Lexical Transactions: an Example

You can nest lexical transactions by including one set of transaction macros inside another, as follows:

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) 
Available on line
This example program is available on line in

Using Dynamic Transactions

You demarcate a dynamic transaction using two methods:

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:

Beginning a Dynamic Transaction

begin() returns a pointer to an os_transaction object that represents the transaction of the current session. You use this object as the implicit this argument when calling other member functions of the class os_transaction.

begin() takes one argument, transaction_type, which specifies the transaction type and can be one of the following enumerators:

It is the user's responsibility to delete the os_transaction object after the transaction has terminated.

Committing a Dynamic Transaction

Call os_transaction::commit() to commit a transaction. If you do not specify the txn argument, it defaults to the current transaction. If the call to commit() occurs in a nested transaction, the current transaction is the innermost transaction in which the call occurs. To commit an outer transaction, specify the txn argument.

Dynamic vs. Lexical Transactions

The following are the advantages and disadvantages of the two types of transactions:

Example of a Dynamic Transaction

The following example of a dynamic transaction replaces the lexical transaction portion of the code in the program listed in Example of a Lexical Transaction. Because the boundaries of a dynamic transaction are defined at run time, this version of the program can decide whether to continue with the transaction, commit it, or abort and discard all changes to persistent memory; see the if-else statements near the end of the transaction.

// 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 
Available on line
This example program is available on line in

Locking

Like most database systems, ObjectStore tries to interleave the operations of different processes' transactions to maximize concurrent usage of resources. When scheduling the operations, ObjectStore conforms to a strict two-phase locking discipline (except in the case of multiversion concurrency control; see Multiversion Concurrency Control (MVCC) in Chapter 2 of the Advanced C++ API User Guide). This discipline has been proven correct in that it guarantees serializability; that is, it guarantees that the results of the scheduling are the same as the results of noninterleaved scheduling of the transactions' operations.

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.

For information about setting and retrieving timeout values, see objectstore::set_lock_timeout() and objectstore::get_lock_timeout() in the C++ API Reference.

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:

Aborting the Current Transaction

To force a roll back to the persistent memory state at the beginning of the current transaction, call os_transaction::abort(). This function is static and therefore requires no implicit this argument. If the explicit abort occurs in a nested transaction, the current transaction is the most deeply nested transaction within which the call to abort() occurs.

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:

Aborting the Top-Level Transaction

When you call os_transaction::abort() with no arguments, only the innermost transaction is aborted. You can, however, abort the outermost transaction by calling os_transaction::abort_top_level() with no arguments. This function, like os_transaction::abort(), is static and therefore requires no implicit this argument.

Aborting a Specified Transaction

You also can call os_transaction::abort() with an argument that specifies the transaction you want to abort. The argument is a pointer to a transaction, an instance of os_transaction. To get a pointer to the current transaction (the innermost transaction in which control currently resides), call os_transaction::get_current(), which returns the pointer. To get a pointer to the transaction in which the transaction specified by the this argument is directly nested, call os_transaction::get_parent(), which returns the pointer.

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

Explicit Aborts and Lexical Transactions

When a lexical transaction is aborted because of a call to os_transaction::abort(), control passes to the first statement outside the aborted transaction; that is, the statement just after the OS_END_TXN() macro. At this point, your application can determine whether the transaction was aborted explicitly by examining the second argument to the OS_BEGIN_TXN() macro. To access this argument, supply the address of a tix_exception pointer variable as the argument. If the transaction is aborted for any reason, ObjectStore sets this pointer to the exception that caused the abort.

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.

The following code segment illustrates how to check for an explicit abort in a lexical transaction:

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().

The following example starts a global transaction in update mode:

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.

Note
Transactions occurring in different sessions are completely independent of each other.

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.

Transactions that write-access data on multiple Servers commit in two phases: a voting phase and a decision phase. First, all Servers vote on a transaction, indicating whether they are able to commit it. Servers always vote yes unless they have crashed fatally, such as by running out of disk space. If all the votes are yes, the decision is made to commit and all Servers are told to commit. (During normal processing, the Client process is the coordinator of the transaction.)

Problems can still occur, before the transaction completes: a Server might crash in the middle of the commit process, after it has voted but before it receives a decision. Or, a Server could crash or be restarted between the vote and decision phases. In both cases, the Server must communicate with another Server to determine the outcome of the transaction, and the coordination control is passed to one of the Servers.

When a Server is restarted after a multi-Server transaction was in the process of committing when the crash occurred (for example, after a power failure), it prints a message such as the following, either into the log file or to standard output.

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 started
The 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 started
Note 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.



[previous] [next]

Copyright © 1999 Object Design, Inc. All rights reserved.

Updated: 03/09/99 13:54:47