Affects: spring version: 5.2.2

Hi team,

I found register TransactionSynchronizationAdapter in a nested way could fail the inner invocation.

The test case should be like:

@RestController
public class TestController {

    @Resource
    private SomeTestClass someTestClass;

    @Transactional
    @GetMapping("/testTx")
    public String testTx() {
        int a = 3;
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            TransactionSynchronizationManager.registerSynchronization(
                    new TransactionSynchronizationAdapter() {
                        @Override
                        public void afterCommit() {
                            someTestClass.doSomeTx();
                        }
                    });
        } else {
            someTestClass.doSomeTx();
        }
        return "";
    }
}
@Component
public class SomeTestClass {

    public void doSomeTx() {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            TransactionSynchronizationManager.registerSynchronization(
                    new TransactionSynchronizationAdapter() {
                        @Override
                        public void afterCommit() {
                            System.err.println("print 1");
                        }
                    });
        } else {
            System.err.println("print 2");
        }
    }
}

After invoking /testTx, neither print 1 or print 2 is printed out. After I explored the source code, I found the invocation of the outer TransactionSynchronizationAdapter.afterCommit happens in TransactionSynchronizationUtils.invokeAfterCommit, at this time, the threadlocal variable synchronizations is not cleared(isSynchronizationActive() returns true), and the inner registration success. However, after TransactionSynchronizationUtils.invokeAfterCommit, the AbstractPlatformTransactionManager.triggerAfterCompletion is invoked and synchronizations is cleared, no more invocation to the inner afterCommit.

Personally I think this is a issue of the isSynchronizationActive(). This method should judge whether the transaction is already in a after commit state.

Maybe I got it wrong, please figure it out for me.

Thx for your work.

Comment From: snicoll

I am trying to get a picture of what you've described and I think something is missing. Please move that code in text into an actual sample that we can run ourselves. You can do so by attaching a zip to this issue or pushing the code to a separate GitHub repository.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: yizhao1998

HI @snicoll , thanks for your reply. I have prepared a public Git repo containing the code: https://github.com/yizhao1998/tx/.

Please make sure you have a MySQL server available at port 3306 and run sql/run.sql before launching the application. Please let me know if you have any further question or need help with running the application. Thank you.

Comment From: jhoeller

This works as designed since isSynchronizationActive needs to return true for the entire synchronization up until afterCompletion. It's not easy to find out whether a nested synchronization registration will still have certain callbacks involved or not. isSynchronizationActive does not answer that question: a provided afterCommit (or beforeCommit or whatever) implementation might not get invoked anymore. That's why if conditions as shown above are not reliable, unfortunately. This is simply not an intended use case for the transaction synchronization mechanism which is rather designed for resource management.

We could provide a TransactionSynchronizationManager method to check which phase the transaction is in, implying which callbacks are still to be called. This is currently not available and it does not seem worth introducing since it would encourage an unintended usage model where it is still too easy to get the effect wrong.

As a side note, in order to avoid hard TransactionSynchronizationManager references in application code, Spring supports an @TransactionalEventListener arrangement which can defer the processing of published application events to the beforeCommit/afterCommit phase.