Follow @vlad_mihalcea Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!

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 is both a pessimistic and an optimistic locking strategy.

Pessimistic locking entails an additional explicit physical lock taken when loading the entity.

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=? for update][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=? for update][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:

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 another 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:

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=? for update][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]

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.

Insert details about how the information is going to be processed DOWNLOAD NOW