Open nkolosnjaji opened 1 year ago
I would have to investigate how the Spring event publication would actually interact with reactive infrastructure. Unless there's a technical need to really implement the event publication repository in a reactive way, I think it should just be fine to persist event publications in an imperative way.
Interested in this as well.
Regarding Spring Event Publication, even though it is not fully integrated in the reactive stack (see this Open issue), at least some support for non-blocking listeners exists (see this completed issue).
As detailed in the second issue, declaring listeners like this works:
@EventListener
public Mono<Void> handleEvent(Object event) {
...
}
So I think it should be feasible to plug this with a reactive repository.
So I think it should be feasible to plug this with a reactive repository.
Making it work is probably "feasible", but not obvious. In a simple implementation using the provided jdbc
starter for the persistence of events, the r2dbc
transaction manager complains about handling jdbc
queries. Having an r2dbc
starter implementation is probably the most straightforward and obvious solution for this first pitfall.
I am interested in this as well. This is currently blocking the adaption of Spring Modulith for us.
@pcuriel As of Spring Framework 6.1, Spring Event Publication is fully integrated in the reactive stack. See https://github.com/spring-projects/spring-framework/commit/450cc212a2a06d68914ceb934de00179d0580b71
@odrotbohm I think persisting events in an imperative way may be sufficient but (and this is a big one) it is really hard to set up because there are many things to consider.
Configuring a blocking and a reactive data access layer This is the most obvious one: We have to configure two separate data layers in our application. When we use Spring Boot this means we have to set all of these properties to trigger the correct auto-configurations.
spring.r2dbc.url=r2dbc:postgresql://localhost/test
spring.r2dbc.username=dbuser
spring.r2dbc.password=dbpass
# either jpa/jdbc for spring-modulith-events
spring.datasource.url=jdbc:mysql://localhost/test
spring.datasource.username=dbuser
spring.datasource.password=dbpass
# or mongodb
spring.data.mongodb.host=mongoserver1.example.com
spring.data.mongodb.port=27017
spring.data.mongodb.additional-hosts[0]=mongoserver2.example.com:23456
spring.data.mongodb.database=test
spring.data.mongodb.username=user
spring.data.mongodb.password=secret
Multiple Transaction Manager Beans
Spring R2DBC provides a ReactiveTransactionManager
while Spring JDBC/JPA/Mongo provides a PlatformTransactionManager
. When we use Spring Boot, TransactionManager
beans are auto-configured for us. Since we need Spring Data JDBC, JPA or MongoDB on the classpath for Spring Modulith, this means that we have two TransactionManager
beans.
I am not even sure if Spring Boot auto-configures both at the same time. As far as I can tell by only viewing code, this entirely depends on the execution order of the auto-configurations. The
DataSourceTransactionManagerAutoConfiguration
is very strict, theDataSourceTransactionManager
has a@ConditionalOnMissingBean(TransactionManager.class)
. This means that the bean is only auto-configured, if theReactiveTransactionManager
was not auto-configured beforehand.The
R2dbcTransactionManagerAutoConfiguration
is less restrictive because theR2dbcTransactionManager
has a@ConditionalOnMissingBean(ReactiveTransactionManager.class)
. This means it is auto-configured even if aPlatformTransactionManager
was auto-configured beforehand.Spring Modulith Events MongoDB auto-configures a
MongoTransactionManager
which is aPlatformTransactionManager
as well. We don't have any conflicts here due to a very forgiving@ConditionalOnMissingBean
without super type as parameter. So Spring Boot should auto-configure aR2dbcTransactionManager
at the same time without any issues.
Event listeners can not be reactive as long as the event publication repository is blocking
Since our application code is reactive, we have to publish events with the transactional context in the source
property of our events. This is required, because @TransactionalEventListener
accesses the transactional context this way. If it is missing, the event listeners will not be executed at all (unless fallbackExecution
is set to true
). This will be possible starting from Spring Framework 6.1 (see https://github.com/spring-projects/spring-framework/commit/450cc212a2a06d68914ceb934de00179d0580b71).
Now we have to consider the @Transactional
annotation of our event listener itself. Usually, we would like to have a reactive event listener that returns some kind of Publisher
. This implicates that we have to use the R2dbcTransactionManager
again. However, this is not possible, because then the EventPublicationRepository.create
methods runs in a reactive transaction context even though the repository itself is not reactive at all and requires a PlatformTransactionManager
instead. I guess this is the issue @vincentditlevinz encountered. A workaround for this would be not using a Publisher
as return type and configuring the qualifier for the transaction manager to use, like this: @Transactional("transactionManager")
.
Lucky for us, the qualifiers for the reactive and blocking
TransactionManager
beans are actually different:
- Both the Spring JDBC auto-configuration and the Spring Modulith MongoDB auto-configuration use
transactionManager
- The Spring R2DBC auto-configuration uses
connectionFactoryTransactionManager
The bottom-line is: Event listeners cannot be reactive, they have to be blocking due to the transaction management. As far as I understand this shouldn't be an issue as long as event listeners themselves are not doing any blocking I/O or similar CPU-intense tasks. Even then, declaring them as @Async
should work.
I have a working example of a reactive Spring application that uses Spring Modulith. Please keep in mind that the sample uses snapshot versions of Spring Framework 6.1 for the fix of spring-projects/spring-framework#21025. See https://github.com/denniseffing/spring-modulith-test.
I had to jump through a lot of hoops to get this working:
DataSource
. I had to re-enable it manually.ReactiveCrudRepository
s as well and throws an InvalidDataAccessApiUsageException
with the message Reactive Repositories are not supported by JDBC
. To fix this, I had to limit the Spring Data JDBC repository scan manually to the Spring Modulith package.TransactionManager
beans, the @Transactional
annotations in the JdbcEventPublicationRepository
cause a NoUniqueBeanDefinitionException
. I don't know if it's possible to declare a bean that was auto-configured by Spring Boot as @Primary
, so I re-defined the entire TransactionManager
bean instead. This is very hacky, because I copy-pasted the definition from the DataSourceTransactionAutoConfiguration
but it works.I don't think it is feasible to do stuff like this in a production application. It is way too convoluted and complex for too minimal gain. I would recommend dropping reactive programming entirely or dropping Spring Modulith instead. At least until Spring Modulith has first-class support for reactive applications.
@odrotbohm Sorry, the example is in Kotlin again 😇
I can only recommend to hold off experimenting with this for now. As you have already experienced, the current arrangement is not designed to work well on top of a reactive data store. This is primarily due to the way we integrate with Spring's event publication mechanism. It's currently implemented in a very straight-forward way, but at the same time, is exposed to limitations that affect the extent in which we can interact with the data store.
I've been in touch with the Framework team and experts on reactive programming in our broader team, and I think there is a path forward to get this to work cleanly. That said, we're close to 1.0 RC / GA, which means that I'll only get to work on this post those releases. Big feature candidate for 1.1, though.
Not sure for JPA, but it would be nice to have reactive JDBC and Mongo starters.