ObjectStore Java API User Guide
Chapter 5

Working with Transactions

A transaction is a logical unit of work. It is a consistent and reliable portion of the execution of a program. In your code, you place calls to the ObjectStore API to mark the beginnings and ends of transactions. Initial access to a persistent object must always take place inside a transaction. Depending on how the transaction is committed, additional access to persistent objects might be possible.

Either the database is updated with all of a transaction's changes to persistent objects, or the database is not updated at all. If a failure occurs in the middle of a transaction, or you decide to abort the transaction, the contents of the database remain unchanged.

This chapter discusses the following topics:

Starting a Transaction

Working Inside a Transaction

Ending a Transaction

Handling Automatic Transaction Aborts

Determining Transaction Boundaries

Starting a Transaction

ObjectStore provides the COM.odi.Transaction class to represent a transaction. You should not make subclasses of this class.

This section discusses the following topics:

Calling the begin() Method

To start a transaction, call the begin() method on the Transaction class. This returns an instance of Transaction and you can assign it to a variable. The method signature is

Method signature

public static Transaction begin(int type)
The transaction type determines whether ObjectStore waits for a database lock. There can be only one write lock on a database. There can be multiple read-only locks on a database.The type of the transaction can be ObjectStore.UPDATE or ObjectStore.READONLY.

If there is no open database when you start the current transaction, ObjectStore tries to obtain a read lock as soon as the session tries to open a database.

Example

Transaction tr = Transaction.begin(ObjectStore.UPDATE);
This example returns a Transaction object that represents the transaction just started. The result is stored in tr. This is an update transaction, which means that the application can modify database contents.

Allowing Objects to Be Modified in a Transaction

To modify persistent objects, you must specify the transaction type to be ObjectStore.UPDATE. Also, any database you modify must have been opened for update. Note that even if you open a database for read-only, ObjectStore allows you to start an update transaction. An application does not receive an exception until it tries to modify persistent objects inside the read-only database.

If you try to modify persistent data in a read-only transaction, ObjectStore throws UpdateReadOnlyException.

Difference Between Update and Read-Only Transactions

You can start a transaction for READONLY or for UPDATE. The only difference between the two types is that when you start a transaction for READONLY, ObjectStore performs additional checks during the transaction and when you commit the transaction. These checks ensure that changes are not saved in the database if they were made in a read-only transaction. There is no difference in performance between a read-only transaction and an update transaction.

Working Inside a Transaction

A transaction is associated with the session that is associated with the thread that starts the transaction. A transaction remains active until you explicitly commit it or until it aborts. A session can have only one active transaction. Concurrent transactions must be in separate sessions.

This section discusses the following topics:

Separate transactions that access the same database compete with one another for locks on the objects that they access. This can cause one transaction to wait for another to release its locks. Alternatively, it can cause a transaction deadlock situation in which two or more transactions wait for each other. This forces one transaction to abort.

Two transactions can never update the same object at the same time. However, two transactions can both open the same database for update at the same time and they can concurrently make updates to different parts of the database.

Obtaining the Session Associated with the Current Transaction

The current session is the session that a thread most recently joined. To obtain the session that is associated with the current transaction, call the Transaction.getSession() method. The method signature is

public Session Transaction.getSession()
To obtain the transaction that is associated with the current session, call the Session.currentTransaction() method. The method signature is

public Transaction Session.currentTransaction()
To determine whether or not there is a transaction in progress for the current session, call the Transaction.inTransaction() method on Transaction. The method signature is

public static boolean inTransaction()
This method returns true if there is a transaction in progress for the current session. Otherwise, it returns false. It is worth noting that inTransaction() return false if the calling thread is not joined to the current session. This can be important if you use an unassociated thread to check whether there is a transaction and then try to close the database based on a false response. The previously unassociated thread would be automatically joined to the session to close the database. If a transaction is actually in progress, ObjectStore throws TransactionInProgressException, which is, of course, unexpected since inTransaction() returned false.

Transaction Already in Progress

Nested transactions are not allowed. If you try to start a transaction when a transaction for the current session is already in progress, ObjectStore throws TransactionInProgressException.

Obtaining Transaction Objects

An application can obtain the transaction object for the current thread by calling the static current() method on the Transaction class. The method signature is

public static Transaction current()
This method returns the transaction object associated with the current session, for example:

Transaction.current().commit()
This example commits the current transaction. If no transaction is in progress, current() throws NoTransactionInProgressException.

Performing a Transaction Checkpoint

You can use the Transaction.checkpoint() method to commit changes but continue working with the same persistent objects. When you call the checkpoint() method, you specify whether to retain persistent objects as hollow objects or make all persistent objects stale. This is useful when you are trying to improve concurrency, or when you are at a consistent state and want to save your changes but keep working. See Checkpoint: Committing and Continuing a Transaction.

Setting a Transaction Priority

When there is a deadlock, the Server uses the transaction priority as one of the criteria to determine which transaction to abort. See Helping Determine the Transaction Victim in a Deadlock.

Ending a Transaction

When transactions terminate successfully, they commit, and their changes to persistent objects are saved in the database. When transactions terminate unsuccessfully, they abort, and their changes to persistent objects are discarded.

For read-only transactions, there are no advantages to committing them rather than aborting them, nor to aborting them rather than committing them.

This section discusses the following topics:

Committing Transactions

ObjectStore provides the Transaction.commit() method for successfully ending a transaction. When an application commits a transaction, ObjectStore

Transitive persistence

When ObjectStore commits a transaction, it checks to see if there are any transient objects that are referred to by persistent objects. If there are, and if all referred-to objects are persistence-capable objects, ObjectStore stores the referred-to objects in the segments that contains the referring objects. This is the process of transitive persistence. If at least one referred-to object is not persistence-capable, ObjectStore throws ObjectNotPersistenceCapableException.

Also, a transient referred-to object cannot be referenced from more than one segment. All cross-segment references must be to exported objects, which means you must have explicitly migrated the object to one of the segments and specified that it is exported. If ObjectStore finds an unexported object that is referred to from more than one segment, it throws AbortException.

Making objects stale

To commit a transaction and make the state of persistent objects stale, call the commit() method with no argument. The method signature is

public void commit()
For example, tr.commit();

Setting object state

To commit a transaction and be flexible about the state of persistent objects after the transaction, call the commit(retain) method on the transaction. The values you can specify for retain are described in Committing Transactions to Save Modifications. The method signature is

public void commit(int retain)
The following example commits the transaction and specifies that the contents of the active persistent objects should remain available to be read.

tr.commit(ObjectStore.RETAIN_READONLY);

What Can Cause a Transaction Commit to Fail?

When ObjectStore tries to commit a transaction, if ObjectStore encounters any of the situations listed below, it causes the transaction commit to fail. When ObjectStore aborts a transaction commit, it throws AbortException.

Aborting Transactions

ObjectStore provides the Transaction.abort() method for unsuccessfully ending a transaction. An abort can happen explicitly through the Transaction.abort() method or implicitly because a session is terminated or there is a system exception. When an application aborts a transaction, ObjectStore

Transient objects

Only the state of the database is rolled back. The state of transient objects is not undone automatically. For example, if you created new transient objects during the transaction, they still exist after the transaction aborts. Applications are responsible for undoing the states of transient objects. Any form of output that occurred before the abort cannot be undone.

Open databases

If you opened any databases during the transaction, ObjectStore closes them. Any databases that were open before the aborted transaction was started remain open after the abort operation.

Application failure

If an application fails during a transaction, when you restart the application the database is as it was before the transaction started. If an application fails during a transaction commit, when you restart the application either the database is as it was before the transaction that was being committed or the database reflects all the transaction's changes. This depends on how far along in the commit process the application was when it terminated. Either all or none of the transaction's changes are in the database.

abort()

To abort a transaction and set the state of persistent objects to the state specified by Transaction.setDefaultAbortRetain(), call the abort() method. The default state is stale. The method signature is

public void abort()
For example,

tr.abort();

abort(retain)

To abort a transaction and specify a particular state for persistent objects after the transaction, call the abort(retain) method on the transaction. The values you can specify for retain are described in Specifying a Particular State for Persistent Objects. The method signature is

public void abort(int retain)
The following example aborts the transaction and specifies that the contents of the active persistent objects should remain available to be read.

tr.abort(ObjectStore.RETAIN_READONLY);

Handling Automatic Transaction Aborts

ObjectStore sometimes automatically aborts a transaction because

Results of Transaction Abort

When ObjectStore aborts a transaction, it rolls back the persistent state to what it was before the transaction. ObjectStore does not roll back the transient state. Any form of output that occurred before the abort cannot be undone. Therefore, it is generally good practice to perform output outside a transaction.

Description of Transaction Abort Exceptions

When ObjectStore aborts a transaction, it throws an exception that indicates the reason for the abort and whether or not it makes sense to retry the transaction. Here is the hierarchy of transaction abort exceptions:

The superclass of exceptions for transaction abort is AbortException. The superclass of exceptions for which it makes sense to retry the aborted exception is RestartableAbortException. AbortExceptions that do not extend RestartableAbortException indicate that before the transaction can proceed, some action is probably required to correct the problem that caused the exception.

Exceptions that require intervention

Exceptions for which some action is probably required include

If your application receives these exceptions, you should check the situation before you retry the transaction. You should not handle these exceptions with retry loops.

Exceptions that indicate retrying

A RestartableAbortException indicates that it makes sense to retry the aborted transaction immediately without taking any other action. This class has two subclasses, which indicate the particular reason for the abort.

Restarting Aborted Transactions

ObjectStore does not retry transactions that are automatically aborted, even if they extend RestartableAbortException. If you want to retry the transaction when your application receives a RestartableAbortException, you must include code to do this in your application. An example of this code follows. If you run the same example in separate VMs at the same time, it produces deadlocks.

import COM.odi.*;
class DeadlockTest {
      static final int MAX_RETRIES = 10;
      public static void main(String[] args) {
            ObjectStore.initialize(null, null);
            Database db = getDatabase();
            while (true) {
                  test(db);
            }
      }
      static void test(Database db) {
            int retries;
            for (retries = 0; retries < MAX_RETRIES; retries++) {
                  try {
                        Transaction.begin(ObjectStore.UPDATE);
                        Integer value = increment(db);
                        Transaction.current().commit();
                        System.out.println("Value = " + value + ", retries = " + retries);
                        break;
                  }      catch (RestartableAbortException e) {
                  }
            }
            if (retries >= MAX_RETRIES)
                  System.out.println("Gave up after " + retries + " retries.");
      }
      static Database getDatabase() {
            try {
             return Database.open("test.odb", ObjectStore.UPDATE);
            }      catch (DatabaseNotFoundException e) {
                  Database db = Database.create("test.odb", 0664);
                  Transaction.begin(ObjectStore.UPDATE);
                  db.createRoot("root", null);
                  Transaction.current().commit();
                  return db;
            }
      }
      static Integer increment(Database db) {
            Integer value = (Integer)db.getRoot("root");
            if (value == null)
                  value = new Integer(0);
            else
                  value = new Integer(value.intValue() + 1);
            db.setRoot("root", value);
            return value;
      }
}

Handling Deadlocks

A simple deadlock occurs when one transaction holds a lock on a page that another transaction is waiting to access, while at the same time this other transaction holds a lock on a page that the first transaction is waiting to access. Neither process can proceed until the other does. There are other, more complicated forms of deadlock that are analogous.

ObjectStore has a deadlock detection facility that breaks deadlocks, when detected, by aborting one of the transactions involved in the deadlock. By aborting one transaction (the victim), ObjectStore causes its locks to be released so other processes can proceed.

ObjectStore throws DeadlockException when it detects a deadlock. This causes ObjectStore to abort the transaction that causes the deadlock. You can change which transaction ObjectStore aborts by changing the setting of the Deadlock Victim Server parameter.

ObjectStore does not detect deadlocks when the deadlock is distributed across multiple Servers.

Determining Transaction Boundaries

When determining whether or not to commit a transaction, consider database state, whether or not to combine transactions, and interdependencies among cooperating threads.

Inconsistent Database State

You should not commit a transaction if the database is in a logically inconsistent state. A database is considered to be in an inconsistent state if at that moment a just-started transaction would encounter problems upon viewing the current state of the data.

Consider your database to be something that moves from one consistent state to another. You should commit a transaction only when the state is consistent. When is a database consistent? When the answer to this question is yes: If you start your application at this very moment, is the database completely usable exactly the way it is now?

For example, suppose your database contains information about married couples. Couples refer to one another through a spouse field. At a particular moment, suppose a person in the database refers to another person in the database through its spouse field, but that spouse does not refer to the first person. At that moment, the database is in an inconsistent state.

Another inconsistency to avoid is retaining references to objects that are not reachable from within the database. Such a situation can cause trouble if the application fails between transactions or if the garbage collector runs.

When the database state is consistent, you might decide not to commit the transaction. However, if you do not commit, you risk losing changes if ObjectStore aborts the transaction. You should always commit changes before you inform a user or some other interface that a particular task was accomplished.

Combining Transactions

The transaction commit operation requires network interaction with the ObjectStore Server. Consequently, you might be able to improve performance by combining several logical transactions into a single transaction.

To do this, skip the call to commit() and subsequent call to Transaction.begin() for a series of sequential transactions. If many of the same objects are used in the different transactions, the single combined commit operation is more efficient than many small commit operations would be.

However, this strategy means that you risk losing changes if ObjectStore aborts the transaction before you commit it. All the logical transactions up to that point would be rolled back with the current logical transaction. You should always commit changes before you inform a user or some other interface that a particular task was accomplished.

Multiple Cooperating Threads

If your application uses cooperating threads, you must take this into account when determining when to commit transactions. For example, you do not want to create a situation where one thread commits a transaction while a cooperating thread is updating persistent objects. The commit() method might make all persistent objects stale for all cooperating threads. If the commit() method retains persistent objects, ObjectStore discards any modifications to retained persistent objects at the start of the next transaction. You must coordinate the Transaction.begin() and Transaction.commit() operations among cooperating threads.

Synchronizing threads is like having a joint checking account. Suppose the amount in the checking account is $100.00. Your partner writes a check for $50.00. Then you try to cash a check for $75.00. This does not work. It does not matter that it was your partner and not you who wrote the check for $50.00. You and your partner have to cooperate.

Performance Considerations

Committing a transaction, even a read-only transaction, has a certain amount of overhead associated with it. If you have a lot of small transactions. You might want to consider combining some of them into larger transactions.



[previous] [next]

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

Updated: 10/07/98 08:45:35