1

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);
    }
RDM
  • 1,136
  • 3
  • 28
  • 50
  • Please update the question with the next things: 1. How does `ExpireAndSaveTrades` class related to it. 2. What exactly is happened inside `Dao` class' methods (if there are calls to some other classes, please post them too) – amseager Apr 21 '21 at 14:48
  • @amseager - updated the question, I am using spring-data for DB operations – RDM Apr 21 '21 at 14:58
  • Does this answer your question? [Spring @Retryable with stateful Hibernate Object](https://stackoverflow.com/questions/54559143/spring-retryable-with-stateful-hibernate-object) – crizzis Apr 21 '21 at 15:34
  • Thanks @crizzis - I did go though this post..but I couldn't make it to work. Let me try again and check – RDM Apr 21 '21 at 15:58
  • 1
    With stateful retry, only state is maintained; you have to call the method until successful or retries are exhausted; see my answer. – Gary Russell Apr 21 '21 at 18:59

1 Answers1

3

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
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • unfortunately I cannot swap it around and why is it wrong to do it this way ? – RDM Apr 21 '21 at 15:07
  • I didn't fully read the question; my explanation is why you are getting the rolled back exception. With stateful retry, your code has to keep calling; there is no automatic retry invocation; in your case the caller of `expireAndSaveTrades` needs to keep calling until either success, or retries are exhausted. I edited the answer. – Gary Russell Apr 21 '21 at 18:33
  • I added an example. – Gary Russell Apr 21 '21 at 18:58
  • Thank yo so much. So in this example, you are using `@Retryable` but you are not specifying which exception to retry at that point. So upon an exception, the control will go back to the parent class's catch block. In short, this can also be achieved by using `@Retryable` in the parent class i.e. in ServiceCaller class and that way you dont have to write try/catch block. – RDM Apr 21 '21 at 22:03
  • what I was trying to achieve here was if there are multiple db operations (say 6 DB calls are happening), within the Retryer.invoke method and lets say the last DB operation failed, then instead of retrying all of them again, I just wanted to retry that specific DB call to save time – RDM Apr 21 '21 at 22:06
  • No. If there are non-retryable exceptions, the retries are exhausted immediately. With stateful, your code must do the retry calls. – Gary Russell Apr 21 '21 at 22:07
  • You cannot do that if all the db ops are run in the same transaction. If one is rolled back, they all will be. – Gary Russell Apr 21 '21 at 22:07