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?