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.