Search code examples
javaspringtransactionsspring-transactionsspring-retry

Issue with @Transactional and @Retryable


I not able to perform DB operations in a transaction if I add @Retryable from spring-retry library. This is how my code structure looks like:

    public class ExpireAndSaveTrades {
    @Transactional(rollbackFor = MyException.class)
    public void expireAndSaveTrades(List<Trade> trades) {
        try {
            // these two MUST be executed in one transaction
            trades.forEach(trade -> dao.expireTrades(trade));
            dao.saveTrades(trades);
        } catch (Exception e) {
            throw new MyException(e.getMessage(), e);
        }
    }
}

public class Dao {
    @Retryable(value = CannotAcquireLockException.class,
            maxAttempts = 3,
            stateful = true,
            backoff = @Backoff(delay = 300, multiplier = 3))
    public void expireTrades(Trade trade) {
    try {
          tradeRepository.expire(trade.getId(), trade.getNewStopDate());
    } catch (CannotAcquireLockException e) {
          expireTrade(trade);
        }

    }

    @Retryable(value = CannotAcquireLockException.class,
            maxAttempts = 3,
            stateful = true,
            backoff = @Backoff(delay = 300, multiplier = 3))
    public void saveTrades(List<Trades> trades) {
    try {
          tradeRepository.saveAll(trades)
    } catch (CannotAcquireLockException e) {
              saveTrades(trades);
            }
    }
}

public interface TradeRepository extends JpaRepository<Trade, Integer> {
    @Modifying
    @Query(value = "update trade set stop_date=:new_stop_date where id=:id", nativeQuery = true)
    void expire(@Param("id") int id, @Param("new_stop_date") String newStopDate);
}

So there is where I am right now:

  1. Without using stateful (i.e. stateful is set to false by default) - retry happens successfully but then at the end of it, I see this exception: org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only and the data which was updated/saved after multiple retries is rolled back in the database table
  2. stateful = true - retry doesn't happen anymore

I have gone through many SO posts and blogs but couldn't find the solution to my problem. Can anybody here please help me out ?

EDIT: updated my question to add try-catch block With this the spring-retry doesn't kick in ( I know because I added a listener to @Retryable to log the retryContext. I dont see the log getting printed. Also the transaction silently rolls back if there was a CannotAcquireLockException

@Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        LOGGER.info("Retry Context - {}", context);
    }

Solution

  • You are doing retries within transaction; this is wrong and will produce the results you are seeing; you need to swap it around and perform transactions within retries. This is why you get the rollback error when not using stateful.

    If you use stateful retry, all @Retryable does is retain state; the caller of the retryable has to keep calling until success or retry exhaustion.

    EDIT

    Here is an example of using stateful retry

    @Component
    class ServiceCaller {
    
        @Autowired
        Service service;
    
        public void call() {
            try {
                this.service.process();
            }
            catch (IllegalStateException e) {
                System.out.println("retrying...");
                call();
            }
            catch (RuntimeException e) {
                throw e;
            }
        }
    
    }
    
    @Component
    class Service {
    
        @Autowired
        Retryer retryable;
    
        @Transactional
        public void process() {
            retryable.invoke();
        }
    
    }
    
    @Component
    class Retryer {
    
        @Retryable(maxAttempts = 3, stateful = true)
        public void invoke() {
            System.out.println("Invoked");
            throw new IllegalStateException("failed");
        }
    
        @Recover
        public void recover(IllegalStateException e) {
            System.out.println("Retries exhausted");
            throw new RuntimeException(e);
        }
    
    }
    
    Invoked
    retrying...
    Invoked
    retrying...
    Invoked
    retrying...
    Retries exhausted
    ...
    Caused by: java.lang.RuntimeException: java.lang.IllegalStateException: failed
        at com.example.demo.Retryer.recover(So67197577Application.java:84) ~[classes/:na]
    ...
    Caused by: java.lang.IllegalStateException: failed
    

    and, without the @Recover method...

    Invoked
    retrying...
    Invoked
    retrying...
    Invoked
    retrying...
    ...
    Caused by: org.springframework.retry.ExhaustedRetryException: Retry exhausted after last attempt with no recovery path; nested exception is java.lang.IllegalStateException: failed