How does LockModeType.PESSIMISTIC_FORCE_INCREMENT work in JPA and Hibernate

Introduction

In my previous post, I introduced the OPTIMISTIC_FORCE_INCREMENT Lock Mode and we applied it for propagating a child entity version change to a locked parent entity. In this post, I am going to reveal the PESSIMISTIC_FORCE_INCREMENT Lock Mode and compare it with its optimistic counterpart.

More alike than different

As we already found out, the OPTIMISTIC_FORCE_INCREMENT Lock Mode can increment an entity version, even when the current transaction doesn’t modify the locked entity state. For each lock mode, Hibernate defines an associated LockingStrategy and the OPTIMISTIC_FORCE_INCREMENT Lock Mode event is handled by the OptimisticForceIncrementLockingStrategy:

public class OptimisticForceIncrementLockingStrategy implements LockingStrategy {

    //code omitted for brevity

    @Override
    public void lock(Serializable id, Object version, Object object, int timeout, SessionImplementor session) {
        if ( !lockable.isVersioned() ) {
            throw new HibernateException( "[" + lockMode + "] not supported for non-versioned entities [" + lockable.getEntityName() + "]" );
        }
        final EntityEntry entry = session.getPersistenceContext().getEntry( object );
        // Register the EntityIncrementVersionProcess action to run just prior to transaction commit.
        ( (EventSource) session ).getActionQueue().registerProcess( new EntityIncrementVersionProcess( object, entry ) );
    }
}

This strategy registers an EntityIncrementVersionProcess in the current Persistence Context action queue. The locked entity version gets incremented just before completing the current running transaction.

public class EntityIncrementVersionProcess implements BeforeTransactionCompletionProcess {
    
    //code omitted for brevity
    
    @Override
    public void doBeforeTransactionCompletion(SessionImplementor session) {
        final EntityPersister persister = entry.getPersister();
        final Object nextVersion = persister.forceVersionIncrement( entry.getId(), entry.getVersion(), session );
        entry.forceLocked( object, nextVersion );
    }
}

Analogous to OPTIMISTIC_FORCE_INCREMENT, the PESSIMISTIC_FORCE_INCREMENT Lock Mode is handled by the PessimisticForceIncrementLockingStrategy:

public class PessimisticForceIncrementLockingStrategy implements LockingStrategy {

    //code omitted for brevity

    @Override
    public void lock(Serializable id, Object version, Object object, int timeout, SessionImplementor session) {
        if ( !lockable.isVersioned() ) {
            throw new HibernateException( "[" + lockMode + "] not supported for non-versioned entities [" + lockable.getEntityName() + "]" );
        }
        final EntityEntry entry = session.getPersistenceContext().getEntry( object );
        final EntityPersister persister = entry.getPersister();
        final Object nextVersion = persister.forceVersionIncrement( entry.getId(), entry.getVersion(), session );
        entry.forceLocked( object, nextVersion );
    }
}

The locked entity is incremented right away, so these two lock modes execute the same logic but at different times. The PESSIMISTIC_FORCE_INCREMENT naming might lead you into thinking that you are using a pessimistic locking strategy, while in reality this Lock Mode is just an optimistic locking variation.

Pessimistic locking entails explicit physical locks (shared or exclusive), while optimistic locking relies on current transaction isolation level implicit locking instead.

The Repository Use Case

I am going to reuse the previous post exercise and switch to using the PESSIMISTIC_FORCE_INCREMENT Lock Mode. To recap a little bit, our domain model contains:

  • a Repository entity, whose version is increased with every new Commit
  • a Commit entity, encapsulating a single atomical Repository state transition
  • a CommitChange component, encapsulating a single Repository resource change

Concurrent modification prevention

Our system is simultaneously accessed by both Alice and Bob. The Repository entity is always locked, right after it’s being fetched from the database:

private final CountDownLatch startLatch = new CountDownLatch(1);
private final CountDownLatch endLatch = new CountDownLatch(1);

@Test
public void testConcurrentPessimisticForceIncrementLockingWithLockWaiting() throws InterruptedException {
    LOGGER.info("Test Concurrent PESSIMISTIC_FORCE_INCREMENT Lock Mode With Lock Waiting");
    doInTransaction(session -> {
        try {
            Repository repository = (Repository) session.get(Repository.class, 1L);
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(repository);

            executeAsync(() -> doInTransaction(_session -> {
                LOGGER.info("Try to get the Repository row");
                startLatch.countDown();
                Repository _repository = (Repository) _session.get(Repository.class, 1L);
                _session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(_repository);
                Commit _commit = new Commit(_repository);
                _commit.getChanges().add(new Change("index.html", "0a1,2..."));
                _session.persist(_commit);
                _session.flush();
                endLatch.countDown();
            }));
            startLatch.await();
            LOGGER.info("Sleep for 500ms to delay the other transaction PESSIMISTIC_FORCE_INCREMENT Lock Mode acquisition");
            Thread.sleep(500);
            Commit commit = new Commit(repository);
            commit.getChanges().add(new Change("README.txt", "0a1,5..."));
            commit.getChanges().add(new Change("web.xml", "17c17..."));
            session.persist(commit);
        } catch (InterruptedException e) {
            fail("Unexpected failure");
        }
    });
    endLatch.await();
}

This test case generates the following output:

#Alice selects the Repository
Query:{[select lockmodeop0_.id as id1_2_0_, lockmodeop0_.name as name2_2_0_, lockmodeop0_.version as version3_2_0_ from repository lockmodeop0_ where lockmodeop0_.id=?][1]} 

#Alice locks the Repository using a PESSIMISTIC_FORCE_INCREMENT Lock Mode
Query:{[update repository set version=? where id=? and version=?][1,1,0]} 

#Bob tries to get the Repository but the SELECT is blocked by Alice lock 
INFO  [pool-1-thread-1]: c.v.h.m.l.c.LockModePessimisticForceIncrementTest - Try to get the Repository row

#Alice sleeps for 500ms to prove that Bob is waiting for her to release the acquired lock
Sleep for 500ms to delay the other transaction PESSIMISTIC_FORCE_INCREMENT Lock Mode acquisition

#Alice makes two changes and inserts a new Commit
Query:{[insert into commit (id, repository_id) values (default, ?)][1]} 
Query:{[insert into commit_change (commit_id, diff, path) values (?, ?, ?)][1,0a1,5...,README.txt]#The Repository version is bumped up to version 1 and a conflict is raised
Query:{[insert into commit_change (commit_id, diff, path) values (?, ?, ?)][1,17c17...,web.xml]} Query:{[update repository set version=? where id=? and version=?][1,1,0]}

#Alice commits the transaction, therefore releasing all locks
DEBUG [main]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

#Bob Repository SELECT can proceed 
Query:{[select lockmodepe0_.id as id1_2_0_, lockmodepe0_.name as name2_2_0_, lockmodepe0_.version as version3_2_0_ from repository lockmodepe0_ where lockmodepe0_.id=?][1]} 

#Bob can insert his changes
Query:{[update repository set version=? where id=? and version=?][2,1,1]} 
Query:{[insert into commit (id, repository_id) values (default, ?)][1]} 
Query:{[insert into commit_change (commit_id, diff, path) values (?, ?, ?)][2,0a1,2...,index.html]} 

This locking process can be easily visualize din the following diagram:

ExplicitLockingPessimisticForceIncrement

The HSQLDB test database Two-Phase Locking implementation uses course grain table locks whenever a database row is modified.

That’s the reason why Bob is unable to get the read lock on the Repository database row that Alice has just updated. Other databases (e.g. Oracle, PostgreSQL) use MVCC, therefore allowing a SELECT to proceed (using the current modifying transaction undo logs for recreating the previous row state) while blocking conflicting data modifying statements (e.g. updating the Repository row, when other concurrent transaction hasn’t yet committed the locked entity state change).

Fail fast

The instantaneous version incrementation has some interesting benefits:

  • If the version UPDATE succeeds (the exclusive row level lock is acquired), no other concurrent transaction can modify the locked database row. This is the moment when the logical lock (the version incrementation) is upgraded to a physical lock (the database exclusive lock).
  • If the version UPDATE fails (because some other concurrent transaction already committed a version change), our current running transaction can be rolled back at once (as opposed to waiting for the transaction to fail during commit)

The latter use case can be visualized as follows:

ExplicitLockingPessimisticForceIncrementFailFast

For this scenario, we are going to use the following test case:

@Test
public void testConcurrentPessimisticForceIncrementLockingFailFast() throws InterruptedException {
    LOGGER.info("Test Concurrent PESSIMISTIC_FORCE_INCREMENT Lock Mode fail fast");
    doInTransaction(session -> {
        try {
            Repository repository = (Repository) session.get(Repository.class, 1L);

            executeSync(() -> {
                doInTransaction(_session -> {
                    Repository _repository = (Repository) _session.get(Repository.class, 1L);
                    _session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(_repository);
                    Commit _commit = new Commit(_repository);
                    _commit.getChanges().add(new Change("index.html", "0a1,2..."));
                    _session.persist(_commit);
                    _session.flush();
                });
            });
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(repository);
            fail("Should have thrown StaleObjectStateException!");
        } catch (StaleObjectStateException expected) {
            LOGGER.info("Failure: ", expected);
        }
    });
}

Generating the following output:

#Alice selects the Repository
Query:{[select lockmodeop0_.id as id1_2_0_, lockmodeop0_.name as name2_2_0_, lockmodeop0_.version as version3_2_0_ from repository lockmodeop0_ where lockmodeop0_.id=?][1]} 

#Bob selects the Repository too
Query:{[select lockmodepe0_.id as id1_2_0_, lockmodepe0_.name as name2_2_0_, lockmodepe0_.version as version3_2_0_ from repository lockmodepe0_ where lockmodepe0_.id=?][1]} 

#Bob locks the Repository using a PESSIMISTIC_FORCE_INCREMENT Lock Mode
Query:{[update repository set version=? where id=? and version=?][1,1,0]} 

#Bob makes a change and inserts a new Commit
Query:{[insert into commit (id, repository_id) values (default, ?)][1]} 
Query:{[insert into commit_change (commit_id, diff, path) values (?, ?, ?)][1,0a1,2...,index.html]} 

#Bob commits the transaction
DEBUG [pool-3-thread-1]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

#Alice tries to lock the Repository
Query:{[update repository set version=? where id=? and version=?][1,1,0]} 

#Alice cannot lock the Repository, because the version has changed
INFO  [main]: c.v.h.m.l.c.LockModePessimisticForceIncrementTest - Failure: 
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.vladmihalcea.hibernate.masterclass.laboratory.concurrency.LockModePessimisticForceIncrementTest$Repository#1]

If you enjoyed this article, I bet you are going to love my book as well.

Conclusion

Like OPTIMISTIC_FORCE_INCREMENT, the PESSIMISTIC_FORCE_INCREMENT Lock Mode is useful for propagating an entity state change to a parent entity.

While the locking mechanism is similar, the PESSIMISTIC_FORCE_INCREMENT is applied on the spot, allowing the current running transaction to instantaneously evaluate the locking outcome.

Code available on GitHub.

If you liked this article, you might want to subscribe to my newsletter too.

Advertisements

6 thoughts on “How does LockModeType.PESSIMISTIC_FORCE_INCREMENT work in JPA and Hibernate

  1. You have explained two different scenarios in this article, in the first case, the second select statements waits till the first transaction completes, In the second case, the exception is thrown. Would you please explain why these are two different cases when LockModeType is same.

  2. Great post as always.

    I have a question.

    Regarding ‘Concurrent modification prevention’ part, I did some test in PostgreSQL.

    In first transaction:

    begin;
    update “user” set email_address = ‘1@xx.com’ where id = 1;
    <—–

    while in second transaction:

    begin;
    select * from "user" where id = 1;
    <—-

    The 'select' statement in second transaction proceeded as normal.

    So I would argue that

    '''
    #Bob Repository SELECT can proceed
    Query:{[select lockmodepe0_.id as id1_2_0_, lockmodepe0_.name as name2_2_0_, lockmodepe0_.version as version3_2_0_ from repository lockmodepe0_ where lockmodepe0_.id=?][1]}
    '''

    this part could come earlier in the log rather than after the commit of Alice's transaction, yes?

    Of course, the 'update' part of Bob has to wait for the completion of Alice's transaction.

    May I ask which DB did you run your test against?

    Thanks a lot for your time.

    1. For this post I used HSQLDB, as mentioned in the post. Postgres uses MVCC and the reads as not blocked by writes. HSQLDB uses two-phase locking, by default, hence the described behavior.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s