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