The information about transactions is organized in the following manner:
Transactions in a database system serve two general purposes:
Concurrency Control
Transactions support concurrent database access by preventing one process's updates from interfering with another process's reads or updates. ObjectStore's concurrency control facilities prevent this interference by ensuring that transactions have the following properties:
Transaction Commit and Abort
Transactions can terminate in two ways: successfully or unsuccessfully. When they terminate successfully, they commit, and their changes to persistent memory are made permanent and visible. When they terminate unsuccessfully, they abort. There are several kinds of transaction aborts:
This applies to statements that access data in a database, but not to all statements that operate on a database. Statements that create, open, or close a database can be either inside or outside a transaction, although, generally, it is advisable not to open or close a database within a transaction.
Multiversion Concurrency Control (MVCC)
When you use multiversion concurrency control (MVCC), you can perform nonblocking reads of a database, allowing another ObjectStore application to update the database concurrently, with no waiting by either the reader or the writer. If your application contains a transaction that uses a database in a read-only fashion, you might be able to use multiversion concurrency control. See Multiversion Concurrency Control (MVCC) in Chapter 2, Advanced Transactions, of the ObjectStore Advanced C++ API User Guide for more information.
Using Lexical Transactions
You begin and commit lexical transactions with the following macros:
OS_BEGIN_TXN( identifier,exception**,transaction-type)and
OS_END_TXN( identifier)The macro arguments are used (among other things) to concatenate unique names. The details of macro preprocessing differ from compiler to compiler, and in some cases you must enter these macro arguments without white space to ensure that the argument concatenation will work correctly.
These and other ObjectStore macros are described in Chapter 4, System-Supplied Macros, in the ObjectStore C++ API Reference.
identifier is a transaction tag. The only requirement on the tag is that different transactions in the same function must use different tags. (The tags are used to construct statement labels, and so have the same scope as labels in C++.)
Transaction type enumerators
transaction-type is one of the following enumerators, defined in the scope of os_transaction:
Example: a lexical transaction
#include <iostream.h> #include <ostore/ostore.hh> main(int, char **argv) { os_database *db1 = os_database::open( argv[1] ) ; OS_BEGIN_TXN(my_tx_1,0,os_transaction::update) int countp* = (int*)( db1->find_root("count")->get_value() ) ; cout << "Hello, world\n" ; cout << ++*countp << "\n" ; OS_END_TXN(my_tx_1) db1->close() ; }
static os_transaction *begin( os_int32 transaction_type = os_transaction::updateThe statements executed in between the calls are all within the same transaction.
) ; static void commit() ; static void commpwd it( os_transaction* ) ;
The first overloading of commit() commits the current transaction. In the case of nesting, it commits the most nested transaction. The second overloading of commit() commits the specified transaction.
Unlike lexical transactions, if a dynamic transaction is aborted due to deadlock, it is not automatically retried. See Threads and Thread Locking.
Locking
As with 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 the strict two-phase locking discipline (except in the case of multiversion concurrency control as described in Multiversion Concurrency Control (MVCC) in Chapter 2, Advanced Transactions, of the ObjectStore Advanced C++ API User Guide). This discipline has been proven correct in the sense that it guarantees serializability; that is, it guarantees that the results of the schedule will be just the same as the results of noninterleaved scheduling of the transactions' operations.
Waiting for Locks
Roughly 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. As long as it is locked, no other process can access it. The data is not unlocked until the end of the transaction. Database- Compared to Segment-Level Locks
There are different kinds of locking provided by database and segment level locks. As its name implies, a database lock prohibits access to the entire database. A segment-level lock only blocks access to the specific segment affected by the transaction. Read Locks and Write Locks
Locking actually treats reading data differently from writing data. When your process reads a persistent data item (such as a data member or persistent variable), the page on which the item resides is read locked. This prevents other processes from writing to that page, but they are still allowed read access to it. When your process writes a data item, the page on which it resides is write locked unless the transaction is abort_only. If the transaction is abort_only, the client obtains read locks for all pages read or written but does not get any write locks. This prevents other processes from reading or writing to that page. (See Transaction Locking Examples in Chapter 2, Advanced Transactions, of the ObjectStore Advanced C++ API User Guide, as well as os_transaction::abort_only in the ObjectStore C++ API Reference.)
Lock Timeouts
You can set a timeout for read- or write-lock attempts, to limit the amount of time your application will wait to acquire a lock. When the timeout is exceeded, an exception is signaled. Handling the exception allows you to continue with alternative processing, and make a later attempt to acquire the lock. See the set_readlock_timeout() and set_writelock_timeout() members of the classes objectstore, os_database, and os_segment in the ObjectStore C++ API Reference.
Reducing Wait Time
There are a number of ways to minimize the amount of time your process spends waiting for locks. See Reducing Wait Time for Locks in Chapter 2, Advanced Transactions, of the ObjectStore Advanced C++ API User Guide.
Lock Probes
You can determine whether a specified address is read locked, write locked, or unlocked with objectstore::get_lock_status(). See the ObjectStore C++ API Reference.
Explicit Lock Acquisition
Normally, ObjectStore performs locking automatically and transparently to the user. But you can explicitly lock a specified page range for read or write with objectstore::acquire_lock(). See the ObjectStore C++ API Reference.
Organizing Transaction Code
If you make transactions too short, you might be allowing other processes to interfere in a harmful way with your process. That is, if some chunk of your code is grouped into two or more short transactions when it should really be all within a single longer transaction, your process or others could produce incorrect results. Here are some guidelines about how to organize your code into transactions. Guidelines for organizing code within a transaction
In general, you should put a given chunk of code inside a single transaction when
Hiding Intermediate Results
One kind of interference between processes occurs when one process uses some intermediate results of another. Just what constitutes an intermediate result depends on the application. Consider, for example, an imaginary MCAD application. Preventing Other Processes' Changes
Another kind of interference between processes arises when one process relies on the state of persistent memory's being unaffected by other processes for the duration of some operation. Rolling Back to Persistent State
If a transaction aborts, its changes to persistent memory are not made permanent or visible to other processes. After an abort, your program sees persistent memory as it was just before the aborted transaction started. You can abort a specified transaction using members of the class os_transaction. You can also abort a lexical transaction by signaling an exception within the transaction and handling the exception outside the transaction. Aborting the Current Transaction
You can always roll back to the persistent memory state at the beginning of the current transaction (the most deeply nested transaction within which control currently resides) by calling the following member of the class os_transaction:
static void abort() ;For dynamic transactions, control flows to the next statement that follows the abort(). For lexical transactions, control flows to the next statement after the end of the current transaction block.
Persistent data is rolled back to its state as of the beginning of the transaction. In addition, if the aborted transaction is not nested within another transaction, all locks are released, and other processes can access the pages that the aborted transaction accessed.
static void abort_top_level() ;
static void abort(os_transaction*) ;The argument is a pointer to a transaction, an instance of the system-supplied class os_transaction. A pointer to the current transaction (the innermost transaction in which control currently resides) is returned by the static member function os_transaction::get_current().
static os_transaction *get_current() ;A pointer to its parent (the innermost transaction within which it is nested) is returned by the member function get_parent().
os_transaction *get_parent() const ;So, for example, to abort a transaction one level up from the current transaction, you might use the following code:
os_transaction* child_tx = os_transaction::get_current() ; if (child_tx) { parent_tx = child_tx->get_parent() ; if (parent_tx) os_transaction::abort(parent_tx) ; }
main() { os_database *db5 = os_database::open("/user1/db5"); OS_BEGIN_TXN(tx1,0,os_transaction::update) os_typespec *part_type = ...; part *a_wheel = ...; part *a_rim = ...; a_wheel->children -= a_rim; /* in this intermediate state, the wheel has no rim */ /* but this state is not visible to other processes */ a_wheel->children |= new(db5, part_type) part(...); if (!check_cost(a_wheel)) { cout << "change aborted: cost check failed\n"; /* undo the part replacement* / os_transaction::abort(); } /* end if */ OS_END_TXN(tx1) db5->close(); }Since the abort results in control's leaving the scope of the current transaction, the current state of all local transient memory is lost. But transient state that is not local to this scope is unaffected by the abort. You should explicitly roll back or reinitialize such state before the abort, if desired.
The thread-locking facility works by either serializing the transactions of different threads or serializing access by different threads to the ObjectStore run time. No two threads are ever in the ObjectStore run time at the same time.
Thread Safety
ObjectStore supports thread safety using a global mutex. This is a data structure that is used to synchronize threads. One global mutex coordinates all threads within an application. Thus, access to the ObjectStore API is currently serialized with one global mutex. When You Need Thread Locking
If the synchronization coded in your application allows two threads to be within the ObjectStore run time at the same time, you need ObjectStore thread locking. A thread can enter the ObjectStore run time under either of the following circumstances:
static void set_thread_locking(os_boolean) ;To disable ObjectStore thread locking, pass 0 to this function. To determine if ObjectStore thread locking is enabled, use the following member of objectstore:
static os_boolean get_thread_locking() ;If nonzero is returned, ObjectStore thread locking is enabled; if 0 is returned, ObjectStore thread locking is disabled.
The two kinds of transactions have the following characteristics:
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. Although two threads cannot be in the ObjectStore run time at the same time, there can be some interleaving of operations of different threads within a transaction. See Chapter 3, Threads, in the ObjectStore Advanced C++ API User Guide for more information on using threads with ObjectStore.
Costs and Benefits of Global Transactions
Advantages of global transactions
Local transactions usually provide better performance, but for some applications, global transactions might be preferable. Here are some of the benefits of using global transactions:
enum os_transaction_scope { os_transaction::local = 1,os_transaction::global }; static os_transaction::begin( os_int32 type = os_transaction::update, os_int32 scope = os_transaction::local );If you use global transactions, be sure to synchronize the threads so that no thread attempts to access persistent data while another thread is committing or aborting. Place a barrier before the end of the transaction so that all participating threads complete work on persistent data before the end-of-transaction operation is allowed to proceed. If you do not, data corruption and program failure can result.
The exception err_deadlock might be signaled asynchronously in any thread using persistent data; the application must be prepared to handle it. Once err_deadlock is handled in the first thread, any other threads that attempt to use the transaction will also get err_deadlock; in particular, any threads that were waiting for the global lock will wake up and immediately get err_deadlock.
Additional information about nested transactions is in Chapter 2, Advanced Transactions, of the ObjectStore Advanced C++ API User Guide. For further discussion of threads, see Chapter 3, Threads, of that publication.
Updated: 03/31/98 16:58:31