How to retry JPA transactions after an OptimisticLockException

Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?

What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?

Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.

So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!

Introduction

In this article, I will discuss how we can implement an automatic Retry mechanism when dealing with the JPA OptimisticLockException.

You can find the introductory part here and the MongoDB implementation here.

Retry

JPA requires running the Persistence Context code inside a transaction, and if our Transaction Manager catches a RuntimeException, it initiates the rollback process. This makes the Persistence Context unusable since we should discard it along with the rolled-back Transaction.

Therefore it’s safer to retry the business logic operation when we are not within a running transaction.

For this, we altered our @Retry annotation like this:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface Retry {

    Class<? extends Throwable>[] on();

    int times() default 1;
}

The Aspect was also changed to take into consideration the new annotation property.

private Object proceed(
        ProceedingJoinPoint pjp, 
        Retry retryAnnotation) 
    throws Throwable {
    int times = retryAnnotation.times();
    Class<? extends Throwable>[] retryOn = retryAnnotation.on();
    if (times <= 0) {
        throw new IllegalArgumentException(
            "@Retry{times} should be greater than 0!"
        );
    }
    if (retryOn.length <= 0) {
        throw new IllegalArgumentException(
            "@Retry{on} should have at least one Throwable!"
        );
    }

    LOGGER.debug(
        "Proceed with {} retries on {}", 
        times, 
        Arrays.toString(retryOn)
    );

    return tryProceeding(pjp, times, retryOn);
}

This utility is part of my Hypersistence Utils project along with the JPA optimistic concurrency control retry mechanism.

Since it’s already available in Maven Central Repository, you can easily use it by just adding this dependency to your pom.xml:

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-55</artifactId>
    <version>${hypersistence-utils.version}</version>
</dependency>

You can configure the optimistic retry as simply as this:

@Retry(times = 10, on = OptimisticLockException.class)
public Product updateName(
        final Long id, 
        final String name) {
    return transactionTemplate.execute(
        new TransactionCallback<Product>() {
            @Override
            public Product doInTransaction(TransactionStatus status) {
                Product product = entityManager.find(Product.class, id);
                product.setName(name);
               
                LOGGER.info("Updating product {} name to {}", product, name);		
                return product;
            }
        }
    );
}

Running a JUnit test that schedules 10 threads to update the same entity will generate optimistic locking exceptions, and this what the test outputs.

Line 102: INFO  [Thread-3]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class javax.persistence.OptimisticLockException]
Line 103: INFO  [Thread-12]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class javax.persistence.OptimisticLockException]
Line 104: INFO  [Thread-9]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class javax.persistence.OptimisticLockException]
Line 105: INFO  [Thread-6]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class javax.persistence.OptimisticLockException]
Line 109: INFO  [Thread-9]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class javax.persistence.OptimisticLockException]
Line 110: INFO  [Thread-7]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class javax.persistence.OptimisticLockException]
Line 114: INFO  [Thread-3]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class javax.persistence.OptimisticLockException]
Line 115: INFO  [Thread-5]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class javax.persistence.OptimisticLockException]
Line 117: INFO  [Thread-11]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class javax.persistence.OptimisticLockException]
Line 118: INFO  [Thread-6]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class javax.persistence.OptimisticLockException]
Line 123: INFO  [Thread-7]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class javax.persistence.OptimisticLockException]
Line 124: INFO  [Thread-5]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class javax.persistence.OptimisticLockException]
Line 126: INFO  [Thread-11]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class javax.persistence.OptimisticLockException]
Line 129: INFO  [Thread-5]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 7 remaining retries on [class javax.persistence.OptimisticLockException]

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

So we reused the same optimistic locking retry logic we first implemented for our MongoDB batch processors, therefore proving we can easily implement such behavior even for JPA repositories.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

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