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.
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.
At runtime, you can override the default lock levels through the FetchConfiguration interface, described in Section 10.5, “Fetch Configuration”. At the beginning of each datastore transaction, Kodo initializes the persistence manager's fetch configuration with the default lock levels and timeouts described in the previous section. By changing the fetch configuration's locking properties, you can control how objects loaded at different points in the transaction are locked. You can also use the fetch configuration of an individual query or extent to apply your locking changes only to objects loaded through that query or extent.
Controlling locking through the fetch configuration works even during optimistic transactions. At the end of the transaction, Kodo resets the fetch configuration's lock levels to none. You cannot lock objects outside of a transaction.
Example 10.4. Setting Runtime Lock Levels
pm.currentTransaction ().begin (); // load stock we know we're going to update at write lock level KodoQuery kq = (KodoQuery) pm.newQuery (Stock.class, "symbol == :s"); kq.setUnique (true); FetchConfiguration fetch = kq.getFetchConfiguration (); fetch.setReadLockLevel (fetch.LOCK_WRITE); fetch.setLockTimeout (3000); // 3 seconds Stock stock = (Stock) kq.execute (symbol); // load an object we don't need locked at none lock level fetch = ((KodoPersistenceManager) pm).getFetchConfiguration (); fetch.setReadLockLevel (fetch.LOCK_NONE); Market market = (Market) pm.getObjectById (marketId, false); stock.setPrice (market.calculatePrice (stock)); pm.currentTransaction ().commit ();
In addition to allowing you to control implicit locking levels, Kodo provides explicit APIs to lock objects and to retrieve their current lock level:
KodoHelper.getLockLevel(Object obj): Return the numeric level at which the given object is currently locked.
KodoPersistenceManager.lockPersistent(Object obj, int level, long timeout): Lock the given object at the specified lock level and timeout. The level is one of the LOCK_XXX constants defined in KodoPersistenceManager (and mirrored in FetchConfiguration), or a numeric value corresponding to a recognized lock level of the lock manager in use.
KodoPersistenceManager.lockPersistent(Object obj): Same as above, but using the fetch configuration's current write lock level and lock timeout.
KodoPersistenceManager.lockPersistentAll(Collection objs, int level, long timeout): Same as lockPersistent, but locks all of the given objects. The given collection may contain nulls, but may not be null itself.
KodoPersistenceManager.lockPersistentAll(Collection objs): Same as above, but using the fetch configuration's current write lock level and lock timeout.
KodoPersistenceManager.lockPersistentAll(Object objs, int level, long timeout): Same as lockPersistent, but locks all of the given objects. The given array may contain nulls, but may not be null itself.
KodoPersistenceManager.lockPersistentAll(Object objs): Same as above, but using the fetch configuration's current write lock level and lock timeout.
Example 10.5. Using lockPersistent()
pm.currentTransaction ().setOptimistic (true); pm.currentTransaction ().begin (); // override default of not locking during an opt trans to lock stock object KodoPersistenceManager kpm = (KodoPersistenceManager) pm; kpm.lockPersistent (stock, kpm.LOCK_WRITE, -1); stock.setPrice (market.calculatePrice (stock)); pm.currentTransaction ().commit ();
Kodo delegates the actual work of locking objects to the system's kodo.runtime.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: The default. This is an alias for the kodo.jdbc.runtime.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.
none: An alias for the kodo.runtime.NoLockManager, which does not perform any locking at all.
sjvm: An alias for the kodo.runtime.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.
JDO's advanced concepts like lazy-loading and object uniquing create several locking corner-cases. The rules below outline Kodo's implicit locking behavior in these cases.
When an object's state is first read within a transaction, the object is locked at the fetch configuration'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 configuration'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 configuration'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 configuration's current lock levels, not the lock levels of the object owning the field.
Whenever an object is looked up within a transaction (through a query, extent, getObjectById, or previously-unloaded relation), 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 lockPersistent methods, 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.
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 JDO 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 JDO 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.