9.4. Object Locking

Controlling how and when objects are locked is an important part of
maximizing the performance of your application under load. This section
describes Kodo's APIs for explicit locking, as well as its rules for
implicit locking.

9.4.1. Configuring Default Locking

You can control Kodo's default transactional read and write lock levels
through the
kodo.ReadLockLevel and
kodo.WriteLockLevel
configuration properties. Each property accepts a
value of none, read,
write, or a number corresponding to a lock
level defined by the lock
manager in use. These properties apply only to non-optimistic
transactions; during optimistic transactions, Kodo never locks objects
by default.

You can control the default amount of time Kodo will wait when trying
to obtain locks through the
kodo.LockTimeout configuration property. Set this
property to the number of milliseconds you are willing to wait for
a lock before Kodo will throw an exception, or to -1 for no limit. It
defaults to -1.

9.4.2. Configuring Lock Levels at Runtime

At runtime, you can override the default lock levels in JPA
through the org.apache.openjpa.persistence.FetchPlan, or in JDO
through the kodo.jdo.KodoFetchPlan. These
interfaces are described above.
At the beginning of each datastore transaction, Kodo initializes the
EntityManager or
PersistenceManager's fetch plan with the default
lock levels and timeouts described in the previous section. By
changing the fetch plan's locking properties, you can control
how objects loaded at different points in the transaction are locked.
You can also use the fetch plan of an individual
Query or Extent to apply
your locking changes only to objects loaded through that
Query or Extent.

Controlling locking through these runtime APIs works even during
optimistic transactions. At the end of the transaction, Kodo resets
the fetch plan's lock levels to none.
You cannot lock objects outside of a transaction.

9.4.4. Lock Manager

Kodo delegates the actual work of locking objects to the system's
kodo.kernel.LockManager. This plugin is controlled
by the kodo.LockManager
configuration property. You can write your own lock
manager, or use one of the bundled options:

pessimistic: This is an alias for the
kodo.jdbc.kernel.PessimisticLockManager, which uses SELECT FOR UPDATE statements (or the
database's equivalent) to lock the database rows corresponding
to locked objects. This lock manager does not distinguish
between read locks and write locks; all locks are write locks.

The pessimistic LockManager can be
configued to additionally perform the version checking
and incrementing behavior of the version
lock manager described below by setting its
VersionCheckOnReadLock
and VersionUpdateOnWriteLock properties:

none: An alias for the
kodo.kernel.NoneLockManager,
which does not perform any locking at all.

sjvm: An alias for the
kodo.kernel.SingleJVMExclusiveLockManager
. This lock manager uses in-memory mutexes
to obtain exclusive locks on object ids. It does not perform
any database-level locking. Also, it does not distinguish
between read and write locks; all locks are write locks.

version: An alias for the
kodo.kernel.VersionLockManager
. This lock manager does not perform
any exclusive locking, but instead ensures read consistency
by verifying that the version of all read-locked instances
is unchanged at the end of the transaction. Furthermore, a
write lock will force an increment to the version at the
end of the transaction, even if the object is not
otherwise modified. This ensures read consistency with
non-blocking behavior.

This is the default kodo.LockManager
setting in JPA.

Note

In order for the version lock manager
to prevent the dirty read phenomenon, the underlying data
store's transaction isolation level must be set to
the equivalent of "read committed" or higher.

Example 9.7. Disabling Locking

JPA XML format:

<property name="kodo.LockManager" value="none"/>

JDO properties format:

kodo.LockManager: none

9.4.5. Rules for Locking Behavior

When an object's state is first read within a transaction, the
object is locked at the fetch plan's current read lock
level. Future reads of additional lazy state for the object
will use the same read lock level, even if the fetch
plan's level has changed.

When an object's state is first modified within a transaction,
the object is locked at the write lock level in effect when
the object was first read, even if the fetch plan's
level has changed. If the object was not read previously, the
current write lock level is used.

When objects are accessed through a persistent relation field,
the related objects are loaded with the fetch plan's
current lock levels, not the lock levels of the object owning
the field.

Whenever an object is accessed within a transaction,
the object is re-locked at the current read lock
level. The current read and write lock levels become those that
the object "remembers" according to rules one and two above.

If you lock an object explicitly through the APIs demonstrated
above, it is re-locked at the specified level. This level
also becomes both the read and write level that the object
"remembers" according to rules one and two above.

When an object is already locked at a given lock level,
re-locking at a lower level has no effect. Locks cannot be
downgraded during a transaction.

9.4.6. Known Issues and Limitations

Due to performance concerns and database limitations, locking cannot
be perfect. You should be aware of the issues outlined in this
section, as they may affect your application.

Typically, during optimistic transactions Kodo does not
start an actual database transaction until you flush or the
optimistic transaction commits. This allows for very long-lived
transactions without consuming database resources.
When using the default lock manager, however, Kodo must begin a
database transaction whenever you decide to lock an object
during an optimistic transaction. This is because the
default lock manager uses database locks, and databases cannot
lock rows without a transaction in progress. Kodo will log
an INFO message to the kodo.Runtime logging
channel when it begins a datastore transaction just to lock
an object.

In order to maintain reasonable performance levels when
loading object state, Kodo can only guarantee that an
object is locked at the proper lock level after
the state has been retrieved from the database.
This means that it is technically possible for another
transaction to "sneak in" and modify the database record after
Kodo retrieves the state, but before it locks the object. The
only way to positively guarantee that the object is locked and
has the most recent state to refresh the object after locking
it.

When using the default lock manager, the case above can only
occur when Kodo cannot issue the state-loading SELECT as a
locking statement due to database limitations. For example,
some databases cannot lock SELECTs that use joins.
The default lock manager will log an INFO message to the
kodo.Runtime logging channel whenever it
cannot lock the initial SELECT due to database limitations.
By paying attention to these log messages, you can see where
you might consider using an object refresh to guarantee that
you have the most recent state, or where you might rethink the
way you load the state in question to circumvent the database
limitations that prevent Kodo from issuing a locking SELECT in
the first place.