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