2

We're using Spring transactions with JPA and the Resilience4j library for circuit breakers in our application.

Our service, which makes use of the data repositories, has a fallback method defined via the circuit breaker to catch errors and return a fallback response.

The problem is that in case of an error - for example I break the database by brutally deleting the required table at application runtime -, the circuit breaker produces the empty fallback list, but then the rollback exception overrides the response, making the application return an error instead of the (expected) empty list.

I am aware of Spring Data - Never Rollback Readonly Transactions, but it did not help me here.

The service code looks similar to this:

@Transactional(readOnly = true, timeout = 5)
@Component
public class MyService {
  @Autowired
  protected MyRepository dataRepository;  
  ...
  @CircuitBreaker(name = BACKEND_NAME, fallbackMethod = "fallbackList")
  public List<MyEntity> getEmployeeByOrganizationUnit(String code) throws NotFoundException {
    return dataRepository.findAllByCode(code);
  }
  ...
  protected List<MyEntity> fallbackList(final String code, final Throwable cause) throws NotFoundException {
    if (cause instanceof NotFoundException) {
      throw (NotFoundException) cause;
    }
    return Collections.emptyList();
  }
  ...
}

The stack trace in the problem case looks like this:

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
  at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752)
  at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
  at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654)
  at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407)
  at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
  at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:61)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
  at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)
  at com.ubs.hzw.logging.aop.ServiceAuditAdvice.invoke(ServiceAuditAdvice.java:127)
  at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.base/java.lang.reflect.Method.invoke(Method.java:566)
  at ...some advice of ours...
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
  ...

This happens because the UnexpectedRollbackException is thrown after the circuit breaker did call the fallback method.

I investigated this issue and at one point I was wondering why is there a transaction for a read-only request and what does it rollback.

The solution I found to avoid a transaction getting created is to change the propagation of @Transactional to:

@Transactional(readOnly = true, timeout = 5, propagation = Propagation.SUPPORTS)

This means, for read-only operations I am fine with having no transactions (transactions are used, if present).

On operations that modify the data repository I put the (default) propagation = Propagation.REQUIRED into the @Transactional annotation.

My questions now are:

  • Are there any drawbacks when changing propagation to SUPPORTS for purely read-only operations?

  • Is there a better solution to avoid this problem?

JanDasWiesel
  • 382
  • 5
  • 14

0 Answers0