ObjectStore Java API User Guide
Chapter 10
Controlling Concurrency
This chapter provides information about ways that you can control concurrency. The APIs described in this chapter make it easier for your application to access data and less likely that your application must wait to access that data. In addition, you can choose to limit access by other users to the same data.
This chapter discusses the following topics:
Reducing Wait Time for Locks
Using Multiversion Concurrency Control (MVCC)
Checkpoint: Committing and Continuing a Transaction
Locking Objects, Segments, and Databases to Ensure Access
Installing Schema Information in Batch Mode
Reducing Wait Time for Locks
What can you do to reduce the overhead of waiting for locks? One application can reduce the waiting overhead for other concurrent applications by avoiding locking data unnecessarily, and by avoiding locking data for unnecessarily long periods of time. This section describes several techniques for minimizing wait time.
Clustering
One way to help avoid locking data unnecessarily involves not clustering objects together when they are not normally accessed together. Suppose that, during a given transaction, an application requires object-a but not object-b. The two objects are stored on the same page. Since ObjectStore performs page-level locking, when you access one of these objects ObjectStore locks both of them. This prevents other processes from accessing either object until the end of the transaction. If you store object-b in a different segment from object-a, you guarantee that the objects are on different pages. Therefore, the objects will not be locked together.
Transaction Length
Making transactions shorter is one way to avoid locking data for unnecessarily long periods of time. If you do this, you must still ensure that objects in the database are in a consistent state between transactions.
The disadvantage of using shorter transactions is that it can mean using a greater number of transactions. This can increase network overhead, because each transaction commit requires the client to send a commit message to the Server. Nevertheless, this extra network overhead is often outweighed by the savings from shorter waits for locks to be released.
It is sometimes particularly important to make transactions that store new objects in the database or destroy persistent objects as short as possible. This is because many write locks are required, which can decrease the level of concurrency.
Multiversion Concurrency Control (MVCC)
Read-only transactions can use multiversion concurrency control, or MVCC. With MVCC, an application can perform nonblocking reads of a database. This allows another application to update the database concurrently, with no waiting by either the reader or the writer. See Using Multiversion Concurrency Control (MVCC).
Lock Timeouts
Lock timeouts provide the ability to limit the time that ObjectStore waits to obtain a lock. When you try to obtain a lock on an object, segment, or database, you can specify the number of milliseconds for which it is all right to wait for the lock. See Locking Objects, Segments, and Databases to Ensure Access.
Conflicts Caused by Schema Installation
If you find that there are concurrency conflicts caused by incremental schema installation, you can install schema information in batch mode. See Installing Schema Information in Batch Mode.
When an application uses multiversion concurrency control (MVCC), it can perform nonblocking reads of a database. This means that another ObjectStore application can concurrently update the database. Neither the reader nor the writer has to wait for the other. To use MVCC, specify ObjectStore.OPEN_MVCC as the open type when you open a database.
When Is MVCC Appropriate?
MVCC is useful when your application contains a transaction that
How Does MVCC Work?
In each transaction in which an application accesses a database opened for MVCC, it is as if the application were viewing a snapshot of the database. This snapshot
Obtaining Read Locks
When an application has a database opened for MVCC, the application never has to wait for read locks on the database. When an application reads data from a database opened for MVCC, the application never causes other applications to wait for write locks. In addition, when an application accesses a database it has opened for MVCC, the application never causes a deadlock.
Accessing Multiple Databases in a Transaction
When an application reads a database opened for MVCC, the snapshot it views is internally consistent but potentially out of date. This means that the snapshot might not be consistent with other databases accessed in the same transaction. Even two databases, both of which are opened for MVCC, might not be consistent with each other. Updates might be performed on one of the databases in between the times of their snapshots.
Serializability
A snapshot might be out of date by the time an application reads some data. However, if each transaction that accesses a database opened for MVCC accesses only that one database, MVCC retains serializability. Such a transaction views a database state that would have resulted from some serial execution of all transactions. All transactions produce the same effects as would have been produced by the serial execution.
Opening a Database for MVCC Access
To use MVCC, specify OPEN_MVCC as the open mode when you open a database. For example:
Database db = Database.open("myDb.odb", ObjectStore.OPEN_MVCC);
After an application opens a database for MVCC, it can read that database without ever waiting for locks or blocking other applications.
You cannot change the open mode of an open database. To change the type of access to a database, you must close the database and then reopen it with a specification of the new open mode.
If you try to update data in a database that you opened for MVCC, ObjectStore throws UpdateReadOnlyException.
In a session, all cooperating threads use the same access mode for a particular database. For example, a thread cannot open a database for update if a cooperating thread has already opened that database for MVCC. However, if a thread opens a database for MVCC, a thread in another session, that is, a noncooperating thread, can open the same database for update.
In the same transaction, your application can open one or more databases for MVCC and open other databases for read-only or update.
Determining If a Database Is Opened for MVCC
To determine if a database is opened for MVCC, call the Database.getOpenMode() method. The method signature is
public int Database.getOpenMode()
If the database is opened for MVCC, ObjectStore returns the ObjectStore.OPEN_MVCC constant. Otherwise, if the database is open, ObjectStore returns either the ObjectStore.OPEN_UPDATE or ObjectStore.OPEN_READONLY constant.
When ObjectStore opens a database as a result of following a cross-database pointer, the automatic open mode can be ObjectStore.OPEN_MVCC. If it is and there are multiple databases open, it is possible that the databases are not consistent with each other. Each database is always internally consistent.
Updating the Snapshot
In a transaction in which you access a database that you opened for MVCC, you might want to update the snapshot periodically. There are two ways to do this:
To help you decide when to do this, you can find out if, during your transaction, there were any write locks on the database you opened for MVCC. A write lock indicates that another application might have made a modification to the database. To do this, call the Transaction.hasLockContention() method on your in-progress transaction. The method signature is
public boolean hasLockContention()
ObjectStore returns true if a Server involved in your transaction has write-locked an object that was read by your application. PSE and PSE Pro always return false.
With a return value of true, if you commit your transaction and start a new one, or checkpoint your transaction, you might have access to updated data. But you might also have access to the same data if the application that had the write lock either aborted the transaction or did not commit any changes.
Where to Find Additional Information
Additional information about MVCC can be found in the ObjectStore C++ documentation. See ObjectStore Advanced C++ API User Guide, Chapter 2, Advanced Transactions, for information about
With the Transaction.checkpoint() method, you get the effect of committing a transaction and then continuing work in a new transaction in which you have read locks on all or most of the persistent objects that were locked in the committed transaction. This is useful when
Note
This checkpoint differs from a conventional checkpoint. In this checkpoint, an application might not have all the locks after the checkpoint that it had before the checkpoint. The details are explained in the next section.
Caution
If your application checkpoints a transaction while an annotated method is executing, your program might incorrectly access persistent objects after the checkpoint. For more information about this and a workaround, see Troubleshooting Access to Persistent Objects.
Advantages of a Checkpoint
The advantage of a checkpoint is that there is less overhead than when you actually end one transaction and start another. When you checkpoint a transaction, it is as if you committed the transaction and then immediately started a new transaction. But in the new transaction, you already have read locks on most or all of your persistent objects.
If another session is waiting for a write lock on a persistent object that was locked in your transaction, you lose that lock when you checkpoint the transaction. As long as another session is not waiting for a write lock on an object that was associated with your transaction, you reacquire as read locks any locks you had before the checkpoint.
After the checkpoint, the persistent objects are stale, or hollow according to which you specify when you call checkpoint(). If you specify that objects should be hollow, you do not have to start from a root object to set up your access to objects. Your application's access to objects is the same before and after the checkpoint.
After a checkpoint, ObjectStore has read locks on the same objects as before the checkpoint, unless another session was waiting for a write lock on one of these objects. In that case, your transaction loses the lock.
If there were any write locks before the checkpoint, ObjectStore changes them to read locks, or gives them to any sessions waiting for those write locks. Consequently, you might have to wait for locks or you might get a deadlock when you try to update the database again.
Suppose your application calls Transaction.checkpoint() and then the transaction started by the checkpoint() method is aborted. ObjectStore does not commit any changes to the database that were made after the checkpoint operation. Any changes made before the checkpoint remain committed.
Calling the checkpoint() Method
To checkpoint a transaction, call the Transaction.checkpoint() method. The method signature is
public void checkpoint(int retain)
The value of retain can be one of the following:
When to checkpoint
Before you checkpoint a transaction, you must ensure that the database is in a consistent state because the state is made persistent.
Caution
During the checkpoint, you must ensure that no other thread tries to access the database.
There are times when you want to ensure your own access to objects and also limit access to those objects by other sessions. This can be when you want to ensure that
To ensure your own access, you can lock an object, a segment, or a database. This means that
Advantages of locking
The overhead for locking objects is far less than the overhead for reading a database with MVCC. Also, if you want to, you can update a locked object. However, if you lock many individual objects, the overhead might be comparable.
Disadvantages of MVCC
The use of MVCC has some disadvantages:
Note
The COM.odi.useDatabaseLocking property is a PSE/PSE Pro feature. If you are using PSE/PSE Pro as well as OSJI, beware of confusing this property with the OSJI Database.acquireLock() method. This method allows a session to explicitly lock a particular database for exclusive use.
PSE/PSE Pro
The acquireLock() methods do nothing in PSE and PSE Pro.
Description of Acquire Lock Methods
The methods that allow you to acquire locks on objects are described below.
public static void ObjectStore.acquireLock(
Object object, int lockType, int timeoutMillis);
public static void Segment.acquireLock(
int lockType, int timeoutMillis);
public static void Database.acquireLock(
int lockType, int timeoutMillis);
Locking Objects for Read or Write Access
The lockType parameter indicates whether you want to read or update the locked objects. You must specify one of the following:
In a transaction, you can reissue the acquireLock() call to change the lockType.
Specifying the Wait Time for a Lock
If the lock is not available, the timeoutMillis parameter indicates how many milliseconds you are willing to wait for the lock. You can specify
If ObjectStore cannot acquire the lock, either because you do not want to wait, or because the waiting period has been exceeded, ObjectStore throws LockTimeoutException.
Releasing Locks
To release locks, you must end the transaction in which you acquired them. Transaction.checkpoint() also releases locks, however you reacquire locks as read locks if no other session is waiting for a write lock for the objects you had locked.
Locking Peer Objects
When you lock a Java peer object that identifies a persistent C++ object, ObjectStore locks the entire object. However, ObjectStore does not lock subobjects that are logically part of the peer object. For example, when you lock a COM.odi.coll collection, you do not lock the contents of the collection.
You cannot lock Java peer objects that represent transient C++ objects.
Obtaining Information About Concurrency Conflicts
Instances of the LockTimeoutBlocker class represent clients that have been involved in concurrency conflicts.
Client list
To obtain a list of such clients, call the LockTimeoutException.getBlockers() method. The signature is
public LockTimeoutBlocker[] getBlockers()
Lock type
To find out whether a concurrency conflict was for a read lock or a write lock, call LockTimeoutBlocker.getLockType(). This returns ObjectStore.READONLY or ObjectStore.UPDATE. The method signature is
public int getLockType()
Client names
To obtain the name of the client that caused the conflict, call LockTimeoutBlocker.getApplicationName(). The method signature is
public String getApplicationName();
Process IDs
To obtain the process ID of the process running a client that caused a conflict, call the LockTimeoutBlocker.getPID() method. The method signature is
public int getPID()
Host names
To obtain the name of the host for the process that was running the client that caused the conflict, call the LockTimeoutBlocker.getHostName() method. The method signature is
public String getHostName()
Setting the Client Name
To allow other applications to see meaningful client names when ObjectStore throws LockTimeoutException, your application should set a client name for itself. Use the system property COM.odi.applicationName to do this. Specify the name of the application for the current client. Include it in the list of properties that you specify when you create a session.
Helping Determine the Transaction Victim in a Deadlock
When there is a deadlock, the Server must choose a transaction to abort. The Server uses several criteria to pick the victim. One of the criteria is the transaction priority, which you can set with the Transaction.setPriority() method.
To obtain the priority that is assigned to transactions started by the current session, call the Transaction.getPriority() method.
Every client has a transaction priority, which is a value in the range 0 to 0xffff. The default value is 0x8000, which is in the middle of the range. The value 0 has a special meaning.
In a deadlock situation, the ObjectStore Server compares the transaction priorities of clients involved in the deadlock. If the lowest transaction priority is held by only one client, this client is the victim. If the lowest transaction priority is held by more than one client, the Server chooses a victim according to the setting of the Deadlock Victim Server parameter. For information about this parameter, see the ObjectStore C++ Interface documentation, ObjectStore Management, Chapter 2, Server Parameters.
If all transactions in a deadlock have a transaction priority of 0, the Server aborts all of them. While this is not a useful way to run a program, it is useful for debugging. You can run several clients under debuggers, and have them all set their priorities to 0. When a deadlock happens, all of them abort and you can see what each one of them was doing. You should only use a priority of 0 if you want this special debugging behavior.
Sometimes, schema installation causes concurrency conflicts. If it does, you can minimize this by installing schema in batch mode.
By default, ObjectStore stores schema information in databases incrementally as needed. If you want to, you can instruct ObjectStore to install schema information in batch mode before it is actually needed. This section provides information about how to do that. The topics discussed are
Background About Schema Information
Schema information describes the classes of objects that are stored in the database. ObjectStore stores schema information in each database.
Kinds of schema information
When you install ObjectStore, you run the setup program. One of the things this program does is ensure that schema information for the ObjectStore classes is available to your applications. If your application accesses C++ classes, the process of building your Java/C++ application makes additional schema information available. ObjectStore also needs schema information for classes you define and for any third-party libraries that your classes use. The postprocessor creates this schema information.
Incremental and batch installation
ObjectStore dynamically stores schema information in each database as needed. This is referred to as incremental schema installation. If you want to, you can instruct ObjectStore to install internal schema information when it creates the database and application schema information when you invoke a method to do so. In this way, you install schema information before it is needed. This is referred to as batch schema installation.
Advantages and disadvantages
The advantages of batch schema installation are
The disadvantages are
Procedure for Installing Schema in Batch
To perform batch schema installation for a database, follow these steps:
- Use the postprocessor to create one or more objects that identify the application types.
- Create a database with the overloading of Database.create() that allows you to specify batch schema installation.
- Start an update transaction.
- Install the application types in the database. These are the types identified in the object or objects created in step 1.
In the unusual situation where you do not run the postprocessor, you follow a different procedure to perform batch schema installation. See If You Do Not Run the Postprocessor.
Identifying the Application Types
The application types are the persistence-capable classes you define and any persistence-capable classes in third-party libraries that your classes refer to. To install schema information in batch mode, you need to create one or more objects that identify all application types. There are two postprocessor options that you can specify to do this.
Individual types
When you run the postprocessor to make classes you define persistence-capable, specify the -summary option. The format is
-summary gen_class_name
This option instructs the postprocessor to generate a class with the name gen_class_name. This generated class extends the PersistentTypeSummary class. The gen_class_name.getPersistentClasses() method returns a list of the classes that were made persistence-capable in this execution of the postprocessor.
The generated class has a no-arguments constructor that passes arrays to the PersistentTypeSummary class constructor.
Types in libraries
Classes in libraries that have already been postprocessed must have an associated generated class (an associated summary) that identifies the persistence-capable classes in the library. You must do one of the following:
You can create one object that contains all relevant summaries, or you can have two or more summaries, which collectively identify all application types. Object Design recommends that you include the summaries for libraries in the summary the postprocessor creates for the classes it makes persistence-capable. To do this, run the postprocessor and specify the -includesummary option for each library summary. The format for specifying this option is
-includesummary inc_class_name
Replace inc_class_name with the name of a class generated by a previous execution of the postprocessor with the -summary option. You must know the name of the generated class. If you did not postprocess the library yourself, you must get the name of the generated class from the library vendor.
When you specify the -includesummary option, you must also specify the -summary option. The postprocessor includes the specified summaries in the new summary it creates. It does not matter whether or not the postprocessor is also annotating any classes.
Library providers
If you are providing a library that contains persistence-capable classes, you must also provide the summary object that identifies those classes. ObjectStore uses the summary object to install schema information for your library at run time. This allows users to install a new version of a library without having to rebuild the application type summary.
General use
For a given execution of the postprocessor,
Creating a Database with Batch Schema Installation
When you create a database, you determine whether you can perform batch schema installation. To allow batch installation, use this overloading of Database.create():
public static Database create(
String name, int fileMode, int schemaInstallMode);
The name parameter specifies the path name of a file. The path can specify a relative name, fully qualified name, remotely mounted directory, or a host name and host-relative pathname. An ObjectStore Server must be available for the directory that contains the specified file.
The fileMode parameter specifies the access mode for the database. ObjectStore throws AccessViolationException if the access mode does not provide owner write access. Otherwise, ObjectStore ignores the access mode specification when you create a database. This is due to a limitation in the Java implementation.
The schemaInstallMode parameter specifies the schema installation mode for the database. The value of this parameter must be either of the following:
Incremental schema installation is the default. When you use the overloading of Database.create() that does not include a specification for schemaInstallMode, it is as if you specified ObjectStore.INSTALL_SCHEMA_INCREMENTAL.
After you create a database, you cannot change the schema installation mode.
C++ interoperability
If you use Java/C++ interoperability and you specify batch schema installation when you create a database, you must not use C++ to change the schema installation flag. Doing so might cause concurrency conflicts during future access to the database.
Installing Application Types in the Database Schema
If you have one or more PersistentTypeSummary objects that identify your application types, you can install schema information in batch in that database. To do so, ensure that the database is opened for update, start an update transaction, and then invoke the Database.installTypes() method. The signature is
public void installTypes(PersistentTypeSummary summary);
Summary parameter
The summary parameter must be an object that identifies the persistence-capable types used by the application. If there are multiple objects that collectively identify all application types, you must do either of the following:
Typically, you create the summary object with the -summary and -includesummary options to the postprocessor. In unusual circumstances, you might explicitly create a summary object with the COM.odi.PersistentTypeSummary constructor. Information about doing this is in the next section.
Example
For example, suppose you ran the postprocessor with this command:
osjcfp -dest .\osjcfpout Class1 Class2 -summary
COM.xyz.MySummary
In your program, create a database with batch schema installation and invoke installTypes() with an instance of MySummary:
Database db = Database.create(
"mydb.odb",
ObjectStore.OWNER_WRITE,
ObjectStore.INSTALL_SCHEMA_BATCH);
Transaction.begin(ObjectStore.UPDATE);
db.installTypes(new COM.xyz.MySummary());
Transaction.current().commit();
ClassInfo must be registered
The installTypes() method ensures that an instance of the ClassInfo subclass associated with each identified persistence-capable class is properly registered. This is normally done automatically by the postprocessor. If ObjectStore cannot find the registered ClassInfo subclass instance for a class, ObjectStore throws ClassNotRegisteredException. When ObjectStore throws this exception, it does not install any schema information for any classes. So, even if it can find the schema information for 99 out of 100 classes, it does not install any schema information at all if it cannot find schema information for one class.
If ObjectStore detects a problem in the type summary, it throws InvalidSummaryException.
Indirectly identified classes
ObjectStore installs schema information for persistence-capable classes that are directly and indirectly identified. That is, if a class contains a reference to a class that is not identified in the summary, ObjectStore tries to install the schema information for the referenced class. If the referenced class refers to some other class that is not in the summary, ObjectStore tries to install the schema information for that class.
ObjectStore installs schema information for any superclasses of specified classes.
Omitting batch installation
Suppose you specify batch schema installation when you create a database, but you forget to install the application types before you store objects in the database. ObjectStore behaves as though the incremental schema installation flag were set. When you store the objects, it loads any needed schema information.
You can invoke installTypes() at any time and ObjectStore installs schema information for application types if that information is not already in the database.
Omitting a type from the summary
Now suppose you forget to identify a persistence-capable class in the summary that you pass to installTypes(). When you store an object of this class in the database, ObjectStore dynamically stores the schema information for that class.
If You Do Not Run the Postprocessor
In the unusual circumstance that you manually annotate your code instead of running the postprocessor, there is a manual way to create a summary of your application's persistence-capable classes.
The COM.odi.PersistentTypeSummary class allows your application types to be identified. To create your own summary, invoke the PersistentTypeSummary constructor. The signature is
public PersistentTypeSummary(
String[] persistentClasses,
String[] includedLibrarySummaries);
The persistentClasses parameter must be an array of names of classes that are persistence-capable. This parameter can be null. The PersistentTypeSummary.getPersistentClasses() method returns an array of the class names that have been identified as persistence-capable.
The includedLibrarySummaries parameter must be an array of names of classes that extend the PersistentTypeSummary class. Typically, these classes identify the persistence-capable classes in a library. This parameter can be null. The PersistentTypeSummary.getIncludedLibrarySummaries() method returns an array of the class names that contain summaries of persistence-capable classes in libraries.
When you define a class that extends the PersistentTypeSummary class, it must have a no-argument constructor.
Suppose you identify a persistence-capable class in a summary, but that class is not in fact persistence-capable. ObjectStore throws ClassNotRegisteredException at run time if it recognizes this when you invoke the installTypes() method on the database.
[previous] [next]
Copyright © 1998 Object Design, Inc. All rights
reserved.
Updated: 10/07/98 08:46:19