Isolation levels - cojen/TuplDB GitHub Wiki

When using the base API, the isolation level is controlled by the lock mode, which is UPGRADABLE_READ by default. The locks only apply to individual entries (or rows), and for this reason the base API doesn't support the serializable isolation level.

Isolation level Lock mode Read lock type Write lock type
Serializable
Repeatable read UPGRADABLE_READ upgradable exclusive
Repeatable read REPEATABLE_READ shared exclusive
Read committed READ_COMMITTED shared exclusive
Read committed Auto-commit (null transaction) shared exclusive
Read uncommitted READ_UNCOMMITTED none exclusive
UNSAFE none none

For all modes except for UNSAFE, writing an entry acquires an exclusive lock, which is held until the transaction (or scope) finishes.

When reading an entry with a repeatable read isolation level, the associated read lock is held until the transaction (or scope) finishes. When updating an entry using the REPEATABLE_READ lock mode, the associated lock might need to be upgraded from a shared lock to an exclusive lock. Because this is deadlock prone, the upgrade attempt is illegal by default. If desired, the rule can be changed to allow the upgrade to succeed under some circumstances.

When using nested transaction scopes, the held locks which were acquired within the scope are promoted to the parent scope only when the scope commits. If the scope is rolled back, all of the locks acquired within the scope are released after undoing any changes. A scope which commits behaves as if the scope wasn't nested, and a scope which rolls back behaves as if it never existed at all.

Cursor stability

The base cursor API doesn't support cursor stability, except when using a repeatable read isolation level. With the read committed level, the lock is released immediately after the entry has been copied to the cursor's value. To emulate cursor stability with the read committed level, the lock can be explicitly released when advancing the cursor:

Database db = ...
View view = ...

Transaction txn = db.newTransaction(); // UPGRADABLE_READ

try (Cursor c = view.newCursor(txn)) {
    for (LockResult result = c.first(); c.key() != null; result = c.next()) {
        if (<should update>) {
            c.store(...);
        } else if (result.isAcquired()) {
            // Wasn't updated, so release the lock.
            txn.unlock();
        }
    }
    txn.commit();
} finally {
    txn.exit();
}

Table API

The table API builds upon the base API and introduces predicate locking and automatic cursor stability. Predicate locks and row locks are used to implement serializable isolation, which is the default. Predicate locks prevent rows from being inserted or updated into the query results of another transaction, and row locks prevent deletes as usual.

Note than when using the table API, a pure repeatable read isolation level cannot be configured. Instead, the isolation level is promoted to serializable, which is stronger.

When using a scanner:

Isolation level Lock mode Read lock type Write lock type Predicate lock Cursor stability
Serializable UPGRADABLE_READ upgradable exclusive yes yes
Serializable REPEATABLE_READ shared exclusive yes yes
Repeatable read
Read committed READ_COMMITTED shared exclusive yes no
Read committed Auto-commit (null transaction) shared exclusive no no
Read uncommitted READ_UNCOMMITTED none exclusive no no
UNSAFE none none no no

When using an updater, upgradable locks are used instead of shared locks, and cursor stability is supported for all isolation levels. The read uncommitted isolation level isn't supported, and instead the stronger read committed level is used.

Isolation level Lock mode Read lock type Write lock type Predicate lock Cursor stability
Serializable UPGRADABLE_READ upgradable exclusive yes yes
Serializable REPEATABLE_READ upgradable exclusive yes yes
Repeatable read
Read committed READ_COMMITTED upgradable exclusive yes yes
Read committed Auto-commit (null transaction) upgradable exclusive no yes
Read committed READ_UNCOMMITTED upgradable exclusive no yes
Read uncommitted
UNSAFE none none no no

When using either a scanner or an updater, a transaction with an explicit READ_COMMITTED mode uses a predicate lock, and so it's a bit stronger than what the read committed isolation level requires. The predicate lock prevents observing a row more than once if another transaction tries moving it to a different location within the table or secondary index. The other transaction must wait on the predicate lock instead. When using a null transaction, the isolation level is also read committed, but without the predicate lock.