Retry mechanism for optimistic locking (spring data + JPA)
Asked Answered
S

4

13

We decided on using optimistic locking in our web application in order to increase concurrency and without the using of pessimistic locking.

We are now on a lookout for retry solutions.

We would like to have as little impact as possible to our current code base.

One of the solutions we saw on the web is using a retry interceptor with annotation to mark a method as retry able.

Problem is we would like to annotate methods that are having the @Transactional annotation on them but the interceptor fails to retry them for some reason. (the interceptor retries non transactional methods perfectly.)

So:

1) Are there any alternatives for the retry that will have minimum impact on our code?

2) Are there any documentations \ tutorials for that solution?

3) Is it even possible to retry a @Transactional annotated method?

Cheers!

Sackett answered 10/2, 2014 at 8:44 Comment(0)
S
12

Ad 3.

You can use Spring Retry to retry transacted method when a version number or timestamp check failed (optimistic lock occurs).

Configuration

@Configuration
@EnableRetry
public class RetryConfig {

}

Usage

@Retryable(StaleStateException.class)
@Transactional
public void doSomethingWithFoo(Long fooId){
    // read your entity again before changes!
    Foo foo = fooRepository.findOne(fooId);

    foo.setStatus(REJECTED)  // <- sample foo modification

} // commit on method end

Use @Transactional (propagation = Propagation.REQUIRES_NEW) for retrying only the code from annotated method.

Smew answered 7/8, 2017 at 9:11 Comment(3)
“SQLException: Statement closed.” occurred when retiring.Vullo
@Vullo I had the same error as you with Hibernate 5.0.x. It went away with 5.2.14.Final. It took me almost a day to figure that out. :(Supportable
@Jean-FrançoisBeauchef thank you very much, I will try it later.Vullo
U
2

You have two ways to achieve this as follows

Recovering from hibernate optimistic locking exception

OR

Using Spring AOP to Retry Failed Idempotent Concurrent Operations

hope this will help you..!

Unpeopled answered 10/2, 2014 at 9:45 Comment(2)
Ashish, we already tried the second solution, apperantly it is not working when you use it on Transactional methods, do you have any idea why?Sackett
I followed exactly what is being written in josiahgore.blogspot.in/2011/02/… and then i annotated a service method with @RetryConcurrentOperation(exception = HibernateOptimisticLockingFailureException.class, retries = 12) Now, because the method is annotated with @Transactional it doesnt work, it gives a regular optimistic lock exception, on a non transactional method if i annotate it - it works.Sackett
W
0

The reason your retry cannot work is that @Transactional priority is higher than @Aspect.

You should make @Aspect higher priority by implementing Ordered in TryAgainAspect class

Interceptor class:

@Aspect
@Component
public class TryAgainAspect implements Ordered {

private int maxRetries;
private int order = 1;

public void setMaxRetries(int maxRetries) {
    this.maxRetries = maxRetries;
}

public int getOrder() {
    return this.order;
}

@Pointcut("@annotation(IsTryAgain)")
public void retryOnOptFailure() {
}

@Around("retryOnOptFailure()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    MethodSignature msig = (MethodSignature) pjp.getSignature();
    Object target = pjp.getTarget();
    Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
    IsTryAgain annotation = currentMethod.getAnnotation(IsTryAgain.class);
    this.setMaxRetries(annotation.tryTimes());

    int numAttempts = 0;
    do {
        numAttempts++;
        try {
            return pjp.proceed();
        } catch (ObjectOptimisticLockingFailureException | StaleObjectStateException exception) {
            exception.printStackTrace();
            if (numAttempts > maxRetries) {
                throw new NoMoreTryException("System error, all retry failed");
            } else {
                System.out.println("0 === retry ===" + numAttempts + "times");
            }
        }
    } while (numAttempts <= this.maxRetries);

    return null;
}

}

IsTryAgain:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IsTryAgain {
    int tryTimes() default 5;
}

Your service class method should add annotation @IsTryAgain and @Transactional

@IsTryAgain
@Transactional(rollbackFor = Exception.class)
public Product buyProduct(Product product) {
// your business logic 
}
Wilhelm answered 13/7, 2020 at 8:30 Comment(0)
S
0

The answers in this ticket helped me a lot, but I'd like to add my answer to stress a specific mistake that I made:

  • the retryable method must reside in a new class
  • that new class must be a Bean (e.g., the class is @Component-annotated)

Otherwise, SpringBoot did not do any proxying, the method was called as-is, and there was no retrying.

Something like this:

import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class MyRetryer {

    // * must be public because of @Transactional
    // * must be in a separate class or REQUIRES_NEW has no effect due to framework proxying
    @Retryable(maxAttempts = 3, value = OptimisticLockingFailureException.class)
    @Transactional(propagation = REQUIRES_NEW)
    public void trySavingThingsToTheDatabase(SomeEntity entity) {
        log.debug("trySavingThingsToTheDatabase: start");
        try {
            someRepository.save(entity);
            log.debug("trySavingThingsToTheDatabase: success");
        } catch (OptimisticLockingFailureException e) {
            log.debug("trySavingThingsToTheDatabase: temporary exception", e);
            // ignore + try again using Spring Retry
            throw e;
        }
    }
}
Savoirvivre answered 25/7, 2024 at 13:56 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.