Affects: 5.2.9


The problem described here can be reproduced using the following sample project: https://github.com/codependent/transactional-event-sample

In a Spring Boot Webflux application with Reactive Mongodb as repository, I would like to take advantage of Spring's event publishing in a transactional way, thus I tried using TransactionalEventListener. The problem is even though the ReactiveTransactionManager has started an actual transaction, when dealing with the event, ApplicationListenerMethodTransactionalAdapter.onApplicationEvent() considers both TransactionSynchronizationManager.isSynchronizationActive() and TransactionSynchronizationManager.isActualTransactionActive() as false.

Below you can see the logs of this process:

2020-09-23 09:46:14.206 TRACE 32760 --- [ctor-http-nio-2] t.a.AnnotationTransactionAttributeSource : Adding transactional method 'com.codependent.sample.service.UserServiceImpl.create' with attribute: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-23 09:46:14.286 TRACE 32760 --- [ctor-http-nio-2] .s.t.r.TransactionSynchronizationManager : Bound value [org.springframework.data.mongodb.ReactiveMongoResourceHolder@d351c8c7] for key [org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory@209f1e30] to context [2b9f14a3-bcbf-4b45-9c9b-3f9dc689d1af]
2020-09-23 09:46:14.286 TRACE 32760 --- [ctor-http-nio-2] .s.t.r.TransactionSynchronizationManager : Initializing transaction synchronization
2020-09-23 09:46:14.286 TRACE 32760 --- [ctor-http-nio-2] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.codependent.sample.service.UserServiceImpl.create]
2020-09-23 09:46:14.288  INFO 32760 --- [ctor-http-nio-2] c.c.sample.service.UserServiceImpl       : create() isSyncActive false - isTxActive false
2020-09-23 09:46:14.311 DEBUG 32760 --- [ctor-http-nio-2] cationListenerMethodTransactionalAdapter : No transaction is active - skipping org.springframework.context.PayloadApplicationEvent[source=org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@f0ba5602, started on Wed Sep 23 09:37:21 CEST 2020]
2020-09-23 09:46:14.313 DEBUG 32760 --- [ctor-http-nio-2] cationListenerMethodTransactionalAdapter : No transaction is active - skipping org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent[source=User(id=null, name=John Doe)]
2020-09-23 09:46:14.323 DEBUG 32760 --- [ctor-http-nio-2] cationListenerMethodTransactionalAdapter : No transaction is active - skipping org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent[source=User(id=null, name=John Doe)]
2020-09-23 09:46:14.328 TRACE 32760 --- [ctor-http-nio-2] .s.t.r.TransactionSynchronizationManager : Retrieved value [org.springframework.data.mongodb.ReactiveMongoResourceHolder@d351c8c7] for key [org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory@209f1e30] bound to context [2b9f14a3-bcbf-4b45-9c9b-3f9dc689d1af: com.codependent.sample.service.UserServiceImpl.create]
2020-09-23 09:46:14.917  INFO 32760 --- [ntLoopGroup-3-4] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:4, serverValue:198137}] to insight-dev-shard-00-00-otcb8.gcp.mongodb.net:27017
2020-09-23 09:46:14.979 DEBUG 32760 --- [ntLoopGroup-3-4] cationListenerMethodTransactionalAdapter : No transaction is active - skipping org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent[source=User(id=5f6afd4626e83e41147e429a, name=John Doe)]
2020-09-23 09:46:14.979 TRACE 32760 --- [ntLoopGroup-3-4] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.codependent.sample.service.UserServiceImpl.create]
2020-09-23 09:46:15.041 TRACE 32760 --- [ntLoopGroup-3-4] .s.t.r.TransactionSynchronizationManager : Clearing transaction synchronization
2020-09-23 09:46:15.043 TRACE 32760 --- [ntLoopGroup-3-4] .s.t.r.TransactionSynchronizationManager : Removed value [org.springframework.data.mongodb.ReactiveMongoResourceHolder@d351c8c7] for key [org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory@209f1e30] from context [2b9f14a3-bcbf-4b45-9c9b-3f9dc689d1af]

The first four lines show that there is an actual transaction, however, inside my transactional method I print the following (5th log line):

        logger.info("create() isSyncActive {} - isTxActive {}",
                TransactionSynchronizationManager.isSynchronizationActive(),
                TransactionSynchronizationManager.isActualTransactionActive())

which shows create() isSyncActive false - isTxActive false. I seems that TransactionSynchronizationManager isn't considering the ongoing Reactive Mongo transaction and in the 6th log line, when ApplicationListenerMethodTransactionalAdapter.onApplicationEvent() kicks in, it doesn't synchronize the transaction, and just skips it.

MongoDb Transaction config:

@EnableTransactionManagement
@Configuration
class MongoDbConfiguration {

    @Bean
    fun mongoTransactionManager(dbFactory: ReactiveMongoDatabaseFactory) =
            ReactiveMongoTransactionManager(dbFactory)

}

Service and event listener:

@Service
@Transactional
class UserServiceImpl(private val userRepository: UserRepository) : UserService {

    private val logger = LoggerFactory.getLogger(javaClass)

    override fun create(user: User): Mono<User> {
        logger.info("create() isSyncActive {} - isTxActive {}",
                TransactionSynchronizationManager.isSynchronizationActive(),
                TransactionSynchronizationManager.isActualTransactionActive())

        return userRepository.save(user.complete())
    }
@Service
class UserEventListener {

    private val logger = LoggerFactory.getLogger(javaClass)

    @TransactionalEventListener
    @Transactional
    fun userCreated(event: UserCreatedEvent){
        logger.info("userCreated({})", event)
    }
@Document(collection = "sample-user")
data class User (var id: String?, var name: String) : AbstractAggregateRoot<User>(){

    fun complete(): User {
        registerEvent(UserCreatedEvent(name))
        return this
    }

}

In order to use the sample you need a Mongodb 4.x instance with replication enabled (to support transactions), e.g. Mongo Atlas, configuring the appropriate value in application.yml:

spring:
  data:
    mongodb:
      uri: xxx

After starting the application just call: curl -X POST localhost:8080/users -d '{"name": "John Doe"}' -H "content-type: application/json"

Comment From: mp911de

Duplicate of https://jira.spring.io/browse/DATAMONGO-2632.

Reactive event listeners do not participate in the transaction that published the event because there's no mechanism to propagate the transaction context to ApplicationEventMulticaster. Any component that wants to participate in the Reactor Context (holding contextual details) must return a Publisher type. ApplicationEventPublisher.publishEvent(…) returns void hence there's no way how this invocation could get hold of the Reactor Context.

Comment From: snicoll

Thanks for letting us know about the duplicate @mp911de. We'll use this issue to document the support a bit more explicitly.

Comment From: pkgonan

@snicoll @mp911de @jhoeller Hi. I am trying to manage transactions through Spring @Transactional annotation by utilizing ReactiveTransactionManager in R2DBC. If I use @EventListener in this case, can I use transaction with @Transaction annotation? I'm not talking about the @TransactionalEventListener, I'm talking about the @EventListener annotation.

Comment From: jhoeller

As of 6.1, @TransactionalEventListener can be triggered with reactive transactions through adding the transaction context as the event source: https://github.com/spring-projects/spring-framework/issues/27515#issuecomment-1660934318

Comment From: sbrannen

  • superseded by #27515