Optimistic locking retry with MongoDB

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

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

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

As I explained this article, there are many benefits of employing optimistic locking for MongoDB batch processors.

The Spring framework offers very good AOP support and, therefore, makes it easy to implement an automatic retry mechanism that we can use for optimistic locking.

We first define a Retry annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
    Class<? extends Throwable>[] on();

    int times() default 1;
}

and we annotate our business logic methods like

@Retry(
    times = 10, 
    on = org.springframework.dao.OptimisticLockingFailureException.class
)
public Product updateName(Long id, String name) {
    Product product = productRepository.findOne(id);
    product.setName(name);
    LOGGER.info("Updating product {} name to {}", product, name);
    return productRepository.save(product);
}

Then we only need an AOP Aspect to intercept the business logic calls and retry in case of optimistic locking detection.

@Aspect
@Component
public class RetryAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(RetryAspect.class);

    @Around("@annotation(io.hypersistence.utils.spring.annotation.Retry)")
    public Object retry(ProceedingJoinPoint pjp) 
        throws Throwable {
        Retry retryAnnotation = getAnnotation(pjp, Retry.class);
        return (retryAnnotation != null) ? 
            proceed(pjp, retryAnnotation) : 
            proceed(pjp);
    }

    private Object proceed(ProceedingJoinPoint pjp) 
            throws Throwable {
        return pjp.proceed();
    }

    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);
    }

    private Object tryProceeding(
            ProceedingJoinPoint pjp, 
            int times, 
            Class<? extends Throwable>[] retryOn)
        throws Throwable {
        try {
            return proceed(pjp);
        } catch (Throwable throwable) {
            if (isRetryThrowable(throwable, retryOn) && times-- > 0) {
                LOGGER.debug(
                    "Retryable failure was caught, {} remaining retr{} on {}",
                    times,
                    (times == 1 ? "y" : "ies"),
                    Arrays.toString(retryOn)
                );
                return tryProceeding(pjp, times, retryOn);
            }
            throw throwable;
        }
    }

    private boolean isRetryThrowable(
            Throwable throwable, 
            Class<? extends Throwable>[] retryOn) {
        Throwable cause = throwable;
        do {
            for (Class<? extends Throwable> retryThrowable : retryOn) {
                if (retryThrowable.isAssignableFrom(cause.getClass())) {
                    return true;
                }
            }

            if (cause.getCause() == null || cause.getCause() == cause) {
                break;
            } else {
                cause = cause.getCause();
            }
        }
        while (true);
        return false;
    }

    private <T extends Annotation> T getAnnotation(
            ProceedingJoinPoint pjp, 
            Class<T> annotationClass)
        throws NoSuchMethodException {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        T annotation = AnnotationUtils.findAnnotation(
            method, 
            annotationClass
        );

        if (annotation != null) {
            return annotation;
        }

        method = pjp.getTarget().getClass().getMethod(
            pjp.getSignature().getName(),
            signature.getParameterTypes()
        );
        return AnnotationUtils.findAnnotation(
            method, 
            annotationClass
        );
    }
}

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

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

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

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>

The test starts 10 threads competing to save a Product, and this is the test log.

Line 492: INFO  [Thread-9]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 495: INFO  [Thread-3]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 504: INFO  [Thread-8]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 505: INFO  [Thread-11]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 507: INFO  [Thread-10]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 513: INFO  [Thread-5]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 523: INFO  [Thread-4]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 9 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 529: INFO  [Thread-3]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 586: INFO  [Thread-10]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 682: INFO  [Thread-5]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 683: INFO  [Thread-3]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 7 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 686: INFO  [Thread-8]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 8 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 702: INFO  [Thread-3]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 6 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 752: INFO  [Thread-5]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 7 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 756: INFO  [Thread-8]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 7 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
Line 859: INFO  [Thread-5]: v.c.a.OptimisticConcurrencyControlAspect - Optimistic locking detected, 6 remaining retries on [class org.springframework.dao.OptimisticLockingFailureException]
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.