Recently I’ve faced an interesting issue. Got transactional method saving entity to the database - method is called, no exception is thrown - but no data is stored into the db.

 

/* SummaryMaker */

    @EventListener
    @Transactional
    public void createSummary(FileProcessedEvent event) {
        summaryRepository.save(
                new Summary(event.getFileName(), event.getLinesProcessed())
        );
    }

@Transactional is from the Spring framework and repo is spring-data-jpa - so even if there was not active transaction then SimpleJpaRepository (JpaRepository implementation) would create one.
Moreover, I know everything is configured correctly since all the other transactional methods in the project work well.
Is this transaction by any chance read-only? I check that quickly - and it appears it isn’t.
But debugger shows that my transaction is not new (there is an external transaction around it) - and this is my lead.

 

So the expected flow in this case goes as follows:

  1. createSummary() method is called
  2. method needs transaction (@Transactional), so one is created
  3. external transaction is present, so createSummary transaction (let’s call it internal transaction) joins it, as propagation was not specified and default one is Propagation.REQUIRED
  4. Only one commit is expected - the one from the external transaction.

expected transaction flow

 

This is clearly not what is happening in our case, but to understand it we need to find out how and when createSummary() method is called.

 

Context

1) The whole case is about some file processing. There is a FileProcessor with @Transactional processFile() method, which publishes application event when processing is done.

/* FileProcessor */

    @Transactional
    public void processFile(String fileName) {
        // do some processing
        eventPublisher.publish(new FileProcessedEvent(this, fileName, linesProcessed));
    }

 

2) EventPublisher uses spring ApplicationEventPublisher but does a little more. Someone figured out that it will check if a transaction is present and

  • if it is - it will publish after commit
  • if not - it will publish immediately

I can think about three reasons for such a solution:

  • to make the transaction as quick as possible
  • to make sure all the data are already in db, since event may be handled outside the scope of current transaction
  • to make sure that we trigger event only when everything went ok (data is in db, no rollback occurred)

this is how it looks in the code:

/* EventPublisher */

	public void publish(ApplicationEvent event) {
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            publishAfterCommit(event);
        } else {
            publishNonTransactional(event);
        }
    }

    private void publishAfterCommit(final ApplicationEvent event) {
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                publishNonTransactional(event);
            }
        });
    }

    private void publishNonTransactional(ApplicationEvent e) {
        applicationEventPublisher.publishEvent(e);
    }

 

3) FileProcessedEvent is handled by SummaryMaker.createSummary(FileProcessedEvent event) - the method we’ve already seen.

The createSummary() method needs a transaction as well, so here is what is expected:

  • processFile() publishes an event
  • EventPublisher sees that there is an active transaction (createSummary() is not finished yet) so it waits for the commit.
  • after the commit, when transaction is finished, event is published
  • event is handled by SummaryMaker.createSummary() in the scope of a new transaction.

And of course again - this is not what is happening.

 

What is happening

The error assumption is that transaction ends immediately after a commit, when in fact, it isn’t.

To be notified about commit we registered TransactionSynchronization. If we examine org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(DefaultTransactionStatus status) we will see the following steps:

  • before commit actions
  • actual commit
  • after commit actions - where our registered synchronization is called and event is fired.
  • cleanup - where transaction is finished (removed from thread local and stops being active)

so what we achieved with our code was:

actual transaction flow

 

  1. external transaction commits
  2. internal transaction joins the external one
  3. internal transaction does not commit cause the external transaction is active
  4. external transaction is finished

 

Solution

Understanding what happened, we now can see that the solution is simple - we shouldn’t join the existing transaction, and to achieve it we only need to change propagation to REQUIRES_NEW in SummaryMaker

/* SummaryMaker */

	@EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createSummary(FileProcessedEvent event) {
        summaryRepository.save(
                new Summary(event.getFileName(), event.getLinesProcessed())
        );
    }

corrected transaction flow

 

You can check the demo code “here”