Enterprise Java

Hibernate locking patterns – How does PESSIMISTIC_FORCE_INCREMENT Lock Mode work

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(new TransactionCallable<Void>() {
		@Override
		public Void execute(Session session) {
			try {
				Repository repository = (Repository) session.get(Repository.class, 1L);
				session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(repository);

				executeNoWait(new Callable<Void>() {
					@Override
					public Void call() throws Exception {
						return doInTransaction(new TransactionCallable<Void>() {
							@Override
							public Void execute(Session _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();
								return null;
							}
						});
					}
				});
				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);
				return null;
			} catch (InterruptedException e) {
				fail("Unexpected failure");
			}
			return null;
		}
	});
	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<a href="https://vladmihalcea.files.wordpress.com/2015/02/explicitlockingpessimisticforceincrementfailfast.png"><img src="https://vladmihalcea.files.wordpress.com/2015/02/explicitlockingpessimisticforceincrementfailfast.png?w=585" alt="ExplicitLockingPessimisticForceIncrementFailFast" width="585" height="224" class="alignnone size-large wp-image-3955" /></a>
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(new TransactionCallable<Void>() {
		@Override
		public Void execute(Session session) {
			try {
				Repository repository = (Repository) session.get(Repository.class, 1L);

				executeAndWait(new Callable<Void>() {
					@Override
					public Void call() throws Exception {
						return doInTransaction(new TransactionCallable<Void>() {
							@Override
							public Void execute(Session _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();
								return null;
							}
						});
					}
				});
				session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(repository);
				fail("Should have thrown StaleObjectStateException!");
			} catch (StaleObjectStateException expected) {
				LOGGER.info("Failure: ", expected);
			}
			return null;
		}
	});
}

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]

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.

Vlad Mihalcea

Vlad Mihalcea is a software architect passionate about software integration, high scalability and concurrency challenges.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button