0

I have the below configuration in application context xml file

    <bean id="methodMapWithDefaultTxAttributeSource" class="org.springframework.transaction.interceptor.MatchAlwaysTransactionAttributeSource">
        <property name="transactionAttribute" value="PROPAGATION_REQUIRES_NEW,timeout_60"/>
    </bean>
    
    <bean id="methodMapTxInterceptor"
          class="org.springframework.transaction.interceptor.TransactionInterceptor">
        <property name="transactionManager" ref="txManager"/>
        <property name="transactionAttributeSource" ref="methodMapWithDefaultTxAttributeSource"/>
    </bean>

    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="interceptorNames">
            <list>
                <idref bean="retryAdvice"/>
                <idref bean="methodMapTxInterceptor"/>
            </list>
        </property>
        <property name="beanNames">
            <value>service</value>
        </property>
    </bean>
    
    <bean id="txProxyTemplate"
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
        abstract="true">
        <property name="transactionManager" ref="txManager" />
        <property name="transactionAttributes">
            <props>
                <prop key="*">PROPAGATION_REQUIRES_NEW,timeout_60</prop>
            </props>
        </property>
    </bean>
    
    <bean id="manager1" class="package2.Manager1">
        <constructor-arg ref="dataSource"/>
    </bean>
    
    <bean id="manager2" class="package2.Manager2">
        <constructor-arg ref="dataSource"/>
    </bean>
    
    <bean id="manager1TxProxy" parent="txProxyTemplate">
        <property name="proxyTargetClass" value="true" />
        <property name="target" ref="manager1" />
    </bean>
    
    <bean id="manager2TxProxy" parent="txProxyTemplate">
        <property name="proxyTargetClass" value="true" />
        <property name="target" ref="manager2"/>
    </bean>

    <bean id="retryPolicy" class="org.springframework.retry.policy.SimpleRetryPolicy">
        <constructor-arg name="maxAttempts" value="3"/>
    </bean>
    
    <bean id="retryTemplate" class="org.springframework.retry.support.RetryTemplate">
        <property name="retryPolicy" ref="retryPolicy"/>
    </bean>
    
    <bean id="rollbackClassifier" class="org.springframework.classify.BinaryExceptionClassifier">
        <constructor-arg name="typeMap">
            <util:map map-class="java.util.HashMap" key-type="java.lang.Class" value-type="java.lang.Boolean">
                <entry key="java.lang.NullPointerException" value="false"/>
            </util:map>
        </constructor-arg>
        <constructor-arg name="defaultValue" value="true"/>
        <constructor-arg name="traverseCauses" value="true"/>
    </bean>
    
    <bean id="retryAdvice" class="org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor">
        <property name="retryOperations" ref="retryTemplate"/>
        <property name="rollbackClassifier" ref="rollbackClassifier"/>
        <property name="label" value="label"/>
    </bean>
   
    <bean id="service" class="package2.Service">
        <property name="manager1" ref="manager1"/>
        <property name="manager2" ref="manager2TxProxy"/>
    </bean>

As you can see i have wrapped a interceptor chain around Service class method. The goal is add retry and transaction facility to all Service class method. I have modified the Service class below method to throw exception whenever it is called

public void executeWithException() {
        manager1.execute();
        throw new NullPointerException();
        //manager2.execute();
    }

Now in the first try, the interceptor chain has StatefulRetryOperationsInterceptor and TransactionInterceptor and before calling the Service class method transaction is created. The Service class method throws exception and it will retry.

Now in the second retry, the interceptor chain will have only StatefulRetryOperationsInterceptor and not TransactionInterceptor. I feel this is wrong. Even for second retry a new transaction has to be created. The javadoc says that. But is not happening here. The TransactionInterceptor is skipped.

Am i missing some configuration here.

Please help me out. Screenshot of call stacktrace on first retry Screenshot of call stacktrace on second retry

Hi Gary, I tried your example. I created my own transaction manager as shown below

public class MyTransactionManager extends AbstractPlatformTransactionManager {
    private int i = 0;
    
    @Override
    protected Object doGetTransaction() throws TransactionException {
        return new Object();
    }

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
        System.out.println("Transaction" + i);
        i = i + 1;
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) throws TransactionException {
        
    }

    @Override
    protected void doRollback(DefaultTransactionStatus status) throws TransactionException {
        
    }
}

Used it in the xml file

<bean id="txManager" class="package2.MyTransactionManager"/>

Below is the console output

Transaction0
Manager1 Execute
Manager1 Execute
Manager1 Execute
Exception in thread "main" 

As you see transaction doBegin method is called once printing "Transaction0". This shows new transactions are not created for every retry.

Below is the main method

public class Example2 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("example2.xml");
        Service service1 = (Service)context.getBean("service");

        service1.executeWithException();
    }
}

When I debugged the code, TransactionInterceptor is in the chain but it is skipped on subsequent retry.

1 Answers1

0

It makes no sense that the interceptor would change between calls; you must be mistaken.

It works fine for me with similar configuration as yours:

@SpringBootApplication
@ImportResource("so70609332-context.xml")
public class So70609332Application {

    public static void main(String[] args) {
        SpringApplication.run(So70609332Application.class, args);
    }

    @Bean
    TransactionInterceptor txInterceptor(TransactionManager tm) {
        return new TransactionInterceptor(tm, new MatchAlwaysTransactionAttributeSource());
    }

    @Bean
    ApplicationRunner runner(Service service, MyTransactionManager tm) {
        return args -> {
            while (true) {
                try {
                    callIt(service);
                }
                catch (IllegalStateException e) {
                }
                catch (Exception e) {
                    System.out.println(tm.begins);
                    break;
                }
            }

        };
    }

    private void callIt(Service nt) {
        try {
            nt.foo();
        }
        catch (IllegalStateException e) {
            throw e;
        }
    }

}

class Service {

    void foo() {
        System.out.println("called: " + TransactionSynchronizationManager.isActualTransactionActive());
        throw new IllegalStateException();
    }

}

@Component
class MyTransactionManager extends AbstractPlatformTransactionManager {

    int begins;

    @Override
    protected Object doGetTransaction() throws TransactionException {
        return new Object();
    }

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
        this.begins++;
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) throws TransactionException {
    }

    @Override
    protected void doRollback(DefaultTransactionStatus status) throws TransactionException {
    }

}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean
        class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="interceptorNames">
            <list>
                <idref bean="retryAdvice" />
                <idref bean="txInterceptor" />
            </list>
        </property>
        <property name="beanNames">
            <value>service</value>
        </property>
    </bean>

    <bean id="retryTemplate"
        class="org.springframework.retry.support.RetryTemplate">
    </bean>

    <bean id="retryAdvice"
        class="org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor">
        <property name="retryOperations" ref="retryTemplate" />
        <property name="label" value="label" />
    </bean>

    <bean id="service" class="com.example.demo.Service" />

</beans>
called: true
called: true
called: true
3
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • I have added screenshots please look into it. If you see the second screenshot there is no TransactionInterceptor in the call stacktrace. – Sumanth Prabhakar Jan 07 '22 at 03:40
  • Screenshots are not enough to debug something like this - you need to provide an [MCRE](https://stackoverflow.com/help/minimal-reproducible-example) (similar to mine) that exhibits the behavior. – Gary Russell Jan 07 '22 at 03:48
  • I have provided the MCRE. Please go through it and respond – Sumanth Prabhakar Jan 07 '22 at 06:17
  • You need to provide the complete (minimal) project someplace, so I can run it to see what is wrong. – Gary Russell Jan 10 '22 at 14:46
  • Do you know a way through which i can share the whole code. I can share with google drive if you give me your google email address. I don't know any other way. – Sumanth Prabhakar Jan 11 '22 at 13:11
  • Please access the files using the link https://mab.to/ynjimdPtQ. It is a link created by MyAirBridge a file transfer service. Please respond if you can't access. – Sumanth Prabhakar Jan 11 '22 at 13:32
  • I don't want to see the "whole code", just a minimal application that exhibits the behavior you observe; I don't want to join yet another service; why not just post a Gist or small project on GitHub? – Gary Russell Jan 11 '22 at 14:10
  • Its not a whole code just the minimal. You don't have to join the service. You click on the link and it will redirect you to their page and you will see download link. You click download button and you get the files. – Sumanth Prabhakar Jan 11 '22 at 14:47
  • It's not a project - just a bunch of files - I need a complete maven or gradle project to completely replicate what you are seeing, if I create my own pom it won't be the same. – Gary Russell Jan 11 '22 at 14:53
  • I didn't use maven or gradle. I am working on eclipse. I manually added the jars. – Sumanth Prabhakar Jan 11 '22 at 14:57
  • I will come up with maven project for you. Please give me some time – Sumanth Prabhakar Jan 11 '22 at 15:18
  • Here is the new link https://mab.to/mzwSP2XO5. This contains a link to zip folder. It is maven project. – Sumanth Prabhakar Jan 11 '22 at 15:32
  • It's a quirk of using retry with exception classification; suppressing the NPE from rollback means the AOP proxy state remains from the previous invocation. You need to let the exception be thrown to the caller and decide there whether you want to re-invoke the method; similar to my example above . Then, the proxy interceptor chain is reset. – Gary Russell Jan 11 '22 at 19:01
  • The caller has already mentioned that it wants a retry by wrapping it with StatefulRetryOperationsInterceptor and listing the exception. Then why we let the exception be thrown to the caller again and tell him to decide whether you want to re-invoke it or not. Also this is not the case when RetryOperationsIntercepter is used. Please help me out. – Sumanth Prabhakar Jan 12 '22 at 03:16
  • Raeding the javadoc, it looks like it's working as designed. It is intended for retry within transaction (not transaction within retry). `noRollbackFor` means don't propagate the exception to the outer (tx) interceptor (perform the retries in the same transaction, don't rollback) - it is not designed for this use case; with transaction within retry, you must always propagate the exception to the caller and he decides whether to retry. – Gary Russell Jan 12 '22 at 03:17
  • I didn't understand your comment "retry within transaction" and "transaction within retry". Are you saying "retry within transaction" means transaction wrapping the retry so whenever retry happens same transaction is used. And "transaction within retry" means retry wrapping the transaction so whenever retry happens new transaction is created. Am i correct here. Can I use RetryOperationsInterceptor will this interceptor helps my use case. Please help me out. – Sumanth Prabhakar Jan 12 '22 at 03:23
  • In your previous comment at the beginning you are saying StatefulRetryOperationsInterceptor is not meant for "transaction within retry". At the end of the comment you are saying contradicting your first statement. You are saying the interceptor is meant for "transaction within retry". Please clarify – Sumanth Prabhakar Jan 12 '22 at 03:37
  • No; I am saying using `noRollbackFor` is not designed for that scenario; it is designed for when the outer interceptor is the transaction interceptor and, for certain exceptions, retry the method, but don't rollback the transaction - perform the retry within the same transaction. The interceptor can be used before or after the transaction interceptor, but `noRollbackFor` can only be used when the retry interceptor is second. – Gary Russell Jan 12 '22 at 03:40
  • I am unable to understand your answer. Getting confused. Can you explain in details assuming i am complete beginner. And in my case where should i place the while loop. In my case retry interceptor is first and second is transaction interceptor. So its "transaction with retry". – Sumanth Prabhakar Jan 12 '22 at 04:01
  • Also please answer to this question https://stackoverflow.com/questions/70675787/difference-between-statefulretryoperationsinterceptor-and-retryoperationsinterce – Sumanth Prabhakar Jan 12 '22 at 04:06
  • One more time; it's the interceptor order; with tx then retry (retry nested within tx) you can classify exceptions that will not cause the transaction to be rolled back. With retry then tx (tx nested within retry) you cannot classify exceptions; you must always allow the exception to be thrown to the caller. See my example of how to use transactions nested within retry. I will look at your other question tomorrow - it's late here. – Gary Russell Jan 12 '22 at 04:18
  • Good night. Thanks for your valuable time – Sumanth Prabhakar Jan 12 '22 at 04:31