Search code examples
javaspring-boottransactionsrollback

@Transactional(propagation = Propagation.REQUIRES_NEW) not creating the new transaction and instead participating in the previous transaction


Spring always creates the new Transaction for REQUIRES_NEW, if an already existing transaction is present or not. but what is the situation where spring does not create new and instead participates in existing transactions?

Below is the situation where new transaction is not getting created and instead participates in existing.

All the methods, mainMethod, methodA(), methodB() and logExceptionMethod() are in different beans, Ideally, When methodB() fails, exception will get caught in catch block, and logExceptionMethod() will audit the log by creating new transaction.

But here new transaction is not started for the logExceptionMethod() method even though, this method is annotated with requires_new and is Public and in different bean, instead it participates in existing transaction which result in rollback

mainMethod - Propagation.REQUIRES methodA - Propagation.REQUIRES_NEW methodB - not annotated logExceptionMethod - Propagation.REQUIRES_NEW

Flow

    @Transactional
    mainMethod() {
          try{
                methodA();
           } catch(Exception e) {
                   throw e;
           }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    methodA(){
         try{
               methodB()
              }
         catch(Exception e) {
              throw e;
          }
    }
    
    methodB(){
          try {
                 // api call gives exception
                }
           catch(Exception e) {
                logExceptionMethod();
                throw e;
            }
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    logExceptionMethod() {
          // create log entity
          // store entity in db
    }

Logs

DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Creating new transaction with name [mainMethod]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@53f498b2]
        
TRACE o.s.t.i.TransactionInterceptor - 1F2EA187-C884-461C-8183-328A4B7621D0 - Getting transaction for [mainMethod]
        
TRACE o.s.d.r.c.s.TransactionalRepositoryProxyPostProcessor$RepositoryAnnotationTransactionAttributeSource - 1F2EA187-C884-461C-8183-328A4B7621D0 - Adding transactional method 2023-08-11 03:04:13 [qtp1482177069-1382] 
        
TRACE o.s.b.f.s.DefaultListableBeanFactory - 1F2EA187-C884-461C-8183-328A4B7621D0 - Returning cached instance of singleton bean 'transactionManager'
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Found thread-bound EntityManager [SessionImpl(1543398267<open>)] for JPA transaction
        
INFO - 1F2EA187-C884-461C-8183-328A4B7621D0 - Current transaction : mainMethod
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Found thread-bound EntityManager [SessionImpl(1543398267<open>)] for JPA transaction
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Suspending current transaction, creating new transaction with name [methodA]
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Opened new EntityManager [SessionImpl(1186799888<open>)] for JPA transaction
        
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@54c9937c]
        
TRACE o.s.t.i.TransactionInterceptor - 1F2EA187-C884-461C-8183-328A4B7621D0 - Getting transaction for [methodA]
        
INFO- 1F2EA187-C884-461C-8183-328A4B7621D0 - Current transaction : methodA
        
ERROR - 1F2EA187-C884-461C-8183-328A4B7621D0 - Exception Occured 
        
TRACE o.s.d.r.c.s.TransactionalRepositoryProxyPostProcessor$RepositoryAnnotationTransactionAttributeSource - 1F2EA187-C884-461C-8183-328A4B7621D0 - Adding transactional method 'org.springframework.data.jpa.repository.support.SimpleJpaRepository.save' with attribute: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
        
TRACE o.s.b.f.s.DefaultListableBeanFactory - 1F2EA187-C884-461C-8183-328A4B7621D0 - Returning cached instance of singleton bean 
        'transactionManager'
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Found thread-bound EntityManager [SessionImpl(1186799888<open>)] for JPA transaction
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Participating in existing transaction
        
INFO  - 1F2EA187-C884-461C-8183-328A4B7621D0 - Added entry in log
        
TRACE o.s.t.i.TransactionInterceptor - 1F2EA187-C884-461C-8183-328A4B7621D0 - Completing transaction for [methodA] after exception: 
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Initiating transaction rollback
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Rolling back JPA transaction on EntityManager [SessionImpl(1186799888<open>)]
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Closing JPA EntityManager [SessionImpl(1186799888<open>)] after transaction
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Resuming suspended transaction after completion of inner transaction
        
TRACE o.s.t.i.TransactionInterceptor - 1F2EA187-C884-461C-8183-328A4B7621D0 - Completing transaction for [mainMethod] after exception:
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Initiating transaction rollback
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Rolling back JPA transaction on EntityManager [SessionImpl(1543398267<open>)]
        
DEBUG o.s.orm.jpa.JpaTransactionManager - 1F2EA187-C884-461C-8183-328A4B7621D0 - Not closing pre-bound JPA EntityManager after transaction

Now I can use specific noRollbackFor property to prevent rollback the transaction, but still, the main question remains is

  1. why does Spring have not created new transaction?
  2. what is the situation where this scenario can happen?

Solution

  • Why is a new transaction not getting created?

    The issue stems from the fact that transactional properties are applied only when a proxy bean is created. In this case, it appears that a proxy is not being created, leading to the absence of a new transaction.

    The logs indicate that no proxy is created for the class containing the HelperC where logExceptionMethod is present.

    (printed with the help of (ApplicationContext.getBean(className).getClass().getName()) )

    INFO 6F28649F-35D3-4382-9BA8-1AEAFA6E5FF3 ###classname: HelperC
    

    However, CGLIB proxies are created for classes MainClass and SecondClass, where mainMethod and methodA are present.

    INFO 8CBCB131-A16B-4F0D-9C17-AED5E15303CE - ###classname: MainClass$$EnhancerBySpringCGLIB$$b32bbff5
    INFO 6F28649F-35D3-4382-9BA8-1AEAFA6E5FF3 - ###classname: SecondClass$$EnhancerBySpringCGLIB$$4c237172
    

    Why is the proxy not getting created?

    The logs suggest that the bean HelperC is not eligible for processing by all BeanPostProcessors, specifically, it is not eligible for auto-proxying.

    INFO o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'helperA' of type [HelperA] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
    INFO o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'helperB' of type [HelperB] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
    INFO o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'helperC' of type [HelperC] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
    

    This may be due to several reasons: HelperC is considered an infrastructure bean, which is a low-level bean used by the Spring framework itself. The count of registered bean post-processors (beanFactory.getBeanPostProcessorCount()) is less than a certain target count. HelperC is not an instance of BeanPostProcessor. This is determined by the PostProcessorRegistrationDelegate$BeanPostProcessorChecker class.

    Additionally, the methodSecurityConfig class extends below class, which Configuration does not allow to create a proxy and defines beans as low-level beans (infrastructure beans)

    @Configuration(proxyBeanMethods = false)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public class GlobalMethodSecurityConfiguration
    

    which prevents the creation of proxies. The methodSecurityConfig class autowires HelperA, which in turn autowires HelperB, eventually leading to the class containing logExceptionMethod, HelperC, which doesn't get proxied due to these rules.

    Why is HelperA used in methodSecurityConfig in my case?

    HelperA is used in methodSecurityConfig due to some business logic requirements. As a result, methodA from HelperA is autowired, leading to the autowiring of HelperB and subsequently HelperC, which contains logExceptionMethod. However, Spring Security configuration prevents the creation of proxies for these classes.

    Solution:

    To resolve this issue, I followed the below steps

    1. Remove Dependency on HelperA which has the logExceptionMethod:

    Since HelperA is causing the issue by triggering the auto-wiring chain that leads to logExceptionMethod, I tried removing the dependency on HelperA from methodSecurityConfig. This would break the chain and potentially allow the creation of proxies for the relevant classes.

    1. Instead of HelperA started using JPARepository classes to fulfill the requirement in security (this can vary according to the business use case).

    Alternatively, I can shift the logExceptionMethod to some other class as well, which is not getting used in the security context because by default beans used in the security context are not allowed to create proxies

    In summary, the bean responsible for creating the transaction for logExceptionMethod is not being proxied. This is because it is being autowired in the security context, and the configuration in the parent class restricts the creation of proxies in this context.

    https://synyx.de/blog/bean-x-of-type-y-is-not-eligible-for-getting-processed-by-all-beanpostprocessors/

    https://www.baeldung.com/spring-not-eligible-for-auto-proxying

    https://github.com/spring-projects/spring-framework/blob/main/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java#L412