A synthetic (typical?) merge operation handling OptimisticLockException:
try {
transactionTemplate.execute(_ -> entityManager.merge(staleFoo)); // throws OptimisticLockException
}
catch (OptimisticLockException e) {
var freshFoo = reloadAndUpdateFooProperties();
transactionTemplate.execute(_ -> entityManager.merge(freshFoo)); // success
}
The above works fine if there is no active transaction at the time of entry.
transactionTemplate.execute(_ -> {
try {
transactionTemplate.execute(_ -> entityManager.merge(staleFoo)); // throws OptimisticLockException
}
catch (OptimisticLockException e) {
var freshFoo = reloadAndUpdateFooProperties();
transactionTemplate.execute(_ -> entityManager.merge(freshFoo)); // success
}
}); // cause UnexpectedRollbackException
Causes an UnexpectedRollbackException Transaction silently rolled back because it has been marked as rollback-only.
Why would the same code behaves differently depending on if there is an existing active transacton? Is this a bug? or I am using transactions wrongly?
Attached sample project to demonstrate the behaviour.
Comment From: quaff
I recommend you to use @Retryable
from spring-retry
.
Comment From: ctzen
I recommend you to use
@Retryable
fromspring-retry
.
I don't think @Retryable
would solve this.
The example I gave is synthetic. Imagine the merge foo is an utility method which may be called from anywhere.
void mergeFoo(Foo staleFoo) {
try {
transactionTemplate.execute(_ -> entityManager.merge(staleFoo)); // throws OptimisticLockException
}
catch (OptimisticLockException e) {
var freshFoo = reloadAndUpdateFooProperties();
transactionTemplate.execute(_ -> entityManager.merge(freshFoo)); // success
}
}
mergeFoo()
works if there is no active transaction when it is called.
Otherwise, the active transaction would throw UnexpectedRollbackException.
I believe making the method @Retryable
would not have helped.
Comment From: jhoeller
I'm afraid this is indeed due to improper use of an outer transaction boundary: With JPA, a resource-level EntityManager
and its first-level cache is going to be reused for the entire transaction - and is effectively becoming stale in case of such an exception. As a consequence, the original JPA transaction cannot commit anymore in such a scenario. Only a fresh attempt in an independent transaction can succeed. This is effectively also the case without an explicit transaction since every EntityManager
invocation operates independently then. With an outer tranasaction, this can be enforced with PROPAGATION_REQUIRES_NEW
if necessary, providing an inner transaction boundary with an independent resource transaction (including its own EntityManager
resource).
Comment From: ctzen
In other words, there is no way to write an utility method that offers retry upon OptimisticLockException robustly. The only option I can think of is to detect for active transaction upon method entry. Go down the regular merge path if one is present, and only go down the retriable path if no active transaction is present.
Comment From: quaff
void mergeFoo(Foo staleFoo) { try { transactionTemplate.execute(_ -> entityManager.merge(staleFoo)); // throws OptimisticLockException } catch (OptimisticLockException e) { var freshFoo = reloadAndUpdateFooProperties(); transactionTemplate.execute(_ -> entityManager.merge(freshFoo)); // success } }
entityManager.merge(freshFoo))
may throws OptimisticLockException
also if another concurrent transaction update the database before this transaction.
Here is my suggestion
@Retryable(maxAttempts = 3, retryFor = OptimisticLockingFailureException.class)
@Transactional
void mergeFoo(Foo staleFoo) {
var freshFoo = entityManager.find(Foo.class, staleFoo.getId());
freshFoo.setXxx(staleFoo.getXxx());
// entityManager.merge() is not required since freshFoo is managed entity, JPA will synchronize its states at the end of transaction
}
Make sure that retry aspect is applied before transaction aspect, see https://github.com/spring-projects/spring-retry/issues/22