34

Is Spring Retry guaranteed to work with Spring's @Transactional annotation?

Specifically, I'm trying to use @Retryable for optimistic locking. It seems like it would be dependent on the ordering of the AOP proxies that were created. For example, if the calls look like this:

Calling code -> Retry Proxy -> Transaction Proxy -> Actual DB Code

Then it would work correctly, but if the proxies were structured like this:

Calling code -> Transaction Proxy -> Retry Proxy -> Actual DB Code

Then the retry wouldn't work, because the act of closing the transaction is what throws the optmistic locking exception.

In testing, it appeared to generate the first case (retry, then transaction), but I couldn't tell if this was a guaranteed behavior or just lucky.

Cobra1117
  • 1,150
  • 3
  • 11
  • 26
  • A more interesting question, I think, is if @ Transacional will work with a @ Recover annotated method.... it seems it does not! – pakman Feb 08 '20 at 01:41

5 Answers5

19

Found the answer here: https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html#tx-decl-explained Table 2 indicates that the advice for the Transactional annotation has an order of Ordered.LOWEST_PRECEDENCE, which means that it is safe to combine Retryable with Transactional as long as you aren't overriding the order of the advice for either of those annotations. In other words, you can safely use this form:

@Retryable(StaleStateException.class)
@Transactional
public void performDatabaseActions() {
    //Database updates here that may cause an optimistic locking failure 
    //when the transaction closes
}
Vaibhav Miniyar
  • 743
  • 1
  • 7
  • 17
Cobra1117
  • 1,150
  • 3
  • 11
  • 26
  • 1
    But, the advice for the `@Retryable` annotation has an order of `Ordered.LOWEST_PRECEDENCE` too! Looks like your conclusion (about safety of the usage) is incorrect. If both advices for `@Retryable` and `@Transactional` have the same order, the resulting order is undefined (i.e., can't be trusted). So, to make it reliable it is required to change the order of `@Retryable` advice. There are multiple ways to do this, but starting from `spring-retry` 2.0.1 (not released yet) there is new `order` parameter of `@EnableRetry` annotation. – Ruslan Stelmachenko Mar 08 '23 at 15:47
4

If you want to test it independenty and be sure how it behaves then you may have @Transactional @Service, then another service that uses transaction one and just adds retries.

In this case, no matter how much you test you are relying on undocumented behaviour (how exactly annotations processing is ordered). This may change between minor releases, based on order in which independent Spring beans are created, etc etc. In short, you are asking for problems when you mix @Transactional and @Retry on same method.

edit: There is similar answered question https://stackoverflow.com/a/45514794/1849837 with code

@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

In that case it seems to be fine, because no matter what order is (retry then transaction, or transaction or retry) observable behaviour will be the same.

Bartosz Bilicki
  • 12,599
  • 13
  • 71
  • 113
  • 1
    That's roughly the code that I was testing with, but wouldn't the behavior be different if the two proxies are reversed? I thought that `Retryable` essentially creates a try/catch (i.e. "around" advice). So if the transaction is closed OUTSIDE that try/catch, the exception wouldn't be caught by the retry proxy. (Maybe I'm just not understanding how Spring Retry works behind the scenes...) – Cobra1117 Apr 05 '18 at 20:23
4

By default Spring Retry builds advice with the same LOWEST_PRECEDENCE order - take a look at the RetryConfiguration. However, there is a pretty simple way to override this order:

@Configuration
public class MyRetryConfiguration extends RetryConfiguration {
   @Override
   public int getOrder() {
      return Ordered.HIGHEST_PRECEDENCE;
   }
}

Make sure to omit the @EnableRetry annotation to avoid default RetryConfiguration be taken into account.

oceansize
  • 623
  • 3
  • 16
4

In case you are using Spring Boot and you want to use @Retryable, this is what you need to do:

  1. Add the dependency to the pom:
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
  1. Enable retries in your Spring Boot application:
@EnableRetry // <-- Add This
@SpringBootApplication
public class SomeApplication {

    public static void main(String[] args) {
        SpringApplication.run(SomeApplication.class, args);
    }

}
  1. Annotate your method with @Retryable:
@Retryable(value = CannotAcquireLockException.class,
        backoff = @Backoff(delay = 100, maxDelay = 300))
@Transactional(isolation = Isolation.SERIALIZABLE)
public boolean someMethod(String someArg, long otherArg) {
    ...
}

You can annotate the same method with both @Retryable and @Transactional and it will work as expected.

Julian Espinel
  • 2,586
  • 5
  • 26
  • 20
2

At the moment (spring-retry version < 2.0.1), by default the order is undefined as both advices for @Retryable and @Transactional annotations have order = Ordered.LOWEST_PRECEDENCE.

It happens that RetryConfiguration advice implements IntroductionAdvisor interface. There is also method org.springframework.aop.support.AopUtils.findAdvisorsThatCanApply that adds into list all found advisors, but it does it using 2 consecutive loops: at first it adds all advisors that implement IntroductionAdvisor interface and then all other avisors (this is why RetryConfiguration advisor always happen to be before BeanFactoryTransactionAttributeSourceAdvisor in this list). Then org.springframework.core.annotation.AnnotationAwareOrderComparator.sort method is called, but because both advisors have the same order, the method keeps the order of advisors in the list.

So, basically, the reason why @Retryable advisor is applied before @Transactional advisor at the moment is just "by accident", or "implementation detail". It could be changed any moment by changing implementation of AopUtils.findAdvisorsThatCanApply method or AnnotationAwareOrderComparator etc. So, it's better to not rely on this implementation detail in your code. :)

Starting from spring-retry version 2.0.1 (not released yet) there is new order attribute of @EnableRetry annotation that is equal to Ordered.LOWEST_PRECEDENCE - 1 by default to make sure @Retryable advice will always be ordered before @Transactional advice (when @Transactional advice order is default).

The order attribute could also be set to any other value, according to your needs, e.g. Ordered.LOWEST_PRECEDENCE - 4.

@EnableRetry(order = Ordered.LOWEST_PRECEDENCE - 4)
@Configuration
public class MyAppConfiguration {}

With this configuration (and starting from spring-retry 2.0.1 - by default) the order of applying advices will be as this:

@Retryable
  @Transactional
    Your method body
  End of @Transactional
End of @Retryable
Ruslan Stelmachenko
  • 4,987
  • 2
  • 36
  • 51