Entity Loader does not take into consideration a subsequent lock request


#1

Loader loads a data from persistContext no matter what query is used, only the key is matter.
So a lock doesn’t work for an entity which is loaded already by a range query.
Is this mean that developers should think what data are loaded in persistence context before locking? or use optimistic lock when a lock is needed in the middle of transaction.

In this case, actually the lock query is sent to DB and application wait for lock release. However the data is not hydrated from the JDBC resultSet. It comes from persistence context and hard to debug why the lock doesn’t work. It would be good to load data from persistence context without query fetch at least. Then developers know that the data comes from the context.

Thanks.


    List policies = policyRepository.findByStatus(SLEP);
    //...some logic...
    Policy policy = policyRepository.findOneWithLock(100127L);
    policy.setStatus(PolicyStatus.SENDING);
    policyRepository.saveAndFlush(policy);

private Object[] getRow(
....
//If the object is already loaded, return the loaded one
				object = session.getEntityUsingInterceptor( key );
				if ( object != null ) {
					//its already loaded so don't need to hydrate it
					instanceAlreadyLoaded(
							rs,
							i,
							persisters[i],
							key,
							object,
							lockModes[i],
							session
						);
				}
				else {
					object = instanceNotYetLoaded(
							rs,
							i,
							persisters[i],
							descriptors[i].getRowIdAlias(),
							key,
							lockModes[i],
							optionalObjectKey,
							optionalObject,
							hydratedObjects,
							session
						);
				}

#2

It’s because the Persistence Context provides application level repeatable reads.

If you need to lock an entity, do it when you want to load it or upgrade the lock:

entityManager.lock(entity, LockModeType.PESSIMISTIC_WRITE);

#3

Upgrading the lock doesn’t help when the locked entity is already loaded in the Persistence Context in a specific case likes checking previous status.


//Transaction start
 List policies = policyRepository.findByStatus(CREATED);
Policy policy = policyRepository.findOneWithLock(one of policies found);
if (policy.getStatus() != PolicyStatus.CREATED) {
    throw new RuntimeException();
}
policy.setStatus(PolicyStatus.SENDING);
policyRepository.saveAndFlush(policy);
//do some works which only one thread is allowed

Because Hibernate doesn’t hydrated the entity from the ResultSet.
In this case I think setting a new transaction on the locked query or using optimistic lock would be better choice.

I read the article you shared. It helps me a lot to understand how Hibernate works.
Thanks.


#4

When issuing a lock request with a higher lock, Hibernate should lock the underlying record even if it is already managed.


#5

This is not a bug. I mean that developers should think what data are loaded in the Persistence Context when using a lock in order to allow a single thread to run.

Below code run multiple times because Policy with CREATED returned always in race conditions.

//do some works which only one thread is allowed

ThreadA - the status of policy from findOneWithLock is CREATED from the Persistence Context.

List policies = policyRepository.findByStatus(CREATED);
Policy policy = policyRepository.findOneWithLock(one of policies found);
if (policy.getStatus() != PolicyStatus.CREATED) {
    throw new RuntimeException();
}
policy.setStatus(PolicyStatus.SENDING);
policyRepository.saveAndFlush(policy);
//do some works which only one thread is allowed -- run 1 times

ThreadB - the status of policy from findOneWithLock is CREATED from the Persistence Context.

List policies = policyRepository.findByStatus(CREATED);
Policy policy = policyRepository.findOneWithLock(one of policies found);
if (policy.getStatus() != PolicyStatus.CREATED) {
    throw new RuntimeException();
}
policy.setStatus(PolicyStatus.SENDING);
policyRepository.saveAndFlush(policy);
//do some works which only one thread is allowed  -- run 2 times

#6

Nope, there’s no bug. It’s just that you’re not using the right Hibernate locking method but the Spring Data method called findOneWithLock which is supposed to fetch an entity.

Most likely, the Spring Data findOneWithLock calls this EntityManager method:

entityManager.find(Class<T> entityClass, Object primaryKey, LockModeType lockMode)

But, you don’t need to fetch the entity the second time and apply the lock since you already fetched it.

What you need to do is this:

entityManager.lock(policy, LockModeType.PESSIMISTIC_WRITE);

Which will just lock the row without fetching the entity:

SELECT id
FROM   policy
WHERE  id =? 
FOR UPDATE

Just because you are using Spring Data, it does not mean you should never use JPA or Hibernate methods.


#7

I switch the second query to Lock method.
When I run two Threads A,B, the later thread(B) wait and come to the if statement after the end of first thread(A) transaction. However the Policy in thread(B) status is still PolicyStatus.CREATED.
Does entityManager.lock refresh the entity?

List policies = policyRepository.findByStatus(CREATED);

entityManager.lock(one of polices,  LockModeType.PESSIMISTIC_WRITE);
if (policy.getStatus() != PolicyStatus.CREATED) {
    throw new RuntimeException();
}
policy.setStatus(PolicyStatus.SENDING);
policyRepository.saveAndFlush(policy);
//do some works which only one thread is allowed -- run 2 times

#8

When I run two Threads A,B, the later thread(B) wait and come to the if statement after the end of first

Yes, that exactly how database locking works. For more details, check out this article.

However the Policy in thread(B) status is still PolicyStatus.CREATED.

Yes, that’s how MVCC works. Until you commit a Transaction, all other transactions don’t observe any state change unless you are using 2PL, in which case reads will block. For more details, check out this article.

Does entityManager.lock refresh the entity?

No. I does not. Lock, as its name implies, is about locking a database record, either optimistically or pessimistically. Refresh is a totally different concept.

This Hibernate tutorial contains many articles about Concurrency Control. You should read them to better understand the concepts.

This presentation is also worth watching if you prefer videos instead.


#9

Mysql 
A(read committed)											| 	B(read committed)
start transaction											|	start transaction
select * from policy where status='CREATED'					|		
policy#1,2,3		<------- 								|	select * from policy where status='CREATED' 
															|	------> policy#1,2,3
select * from policy where id = 1 for update				|	
															|	select * from policy where id = 1 for update
															|	------ waiting
policy#1(CREATED)   <-------								|						
update policy set status='SENDING' where id = 1				|
															|
commit														|	------ end of waiting
															|	------> policy#1(SENDING)
															|	if (policy.getStatus() != PolicyStatus.CREATED) { throw new RuntimeException(); }

When I tested above in mysql, connection B get a Policy with SENDING status because A committed.
So I though Hibernate uses the result of SELECT FOR UPDATE in B connection. However, it doesn’t use it because as you said Hibernate is application level repeatable reads. Ok. I understand. Then I should clear the loaded entity and next time I should think what entity is loaded. This is my understanding.

Lock method is also the same manner. When lock method is called, SELECT FOR UPDATE query is sent, but the result of SELECT FOR UPDATE is not used because Hibernate is application level repeatable reads. Ok.

All comes from the wrong idea that Hibernate works following DB isolation level, in my case read committed.

Is there anything that I’m misunderstanding?


#10

Hibernate uses the DB isolation level as well just like any framework using JDBC.

The only difference is that Hibernate caches entities, and once you load it, you won’t get any updated version in Read Committed level.

That allows you to prevent lost updates when you are also using optimistic locking.


#11

Your comment and links shared to me helps me a lot to understand the inside of Hibernate.
Thank you. I appreciate your helps.


#12

I’m glad I couldd help.