spring-projects / spring-modulith

Modular applications with Spring Boot
https://spring.io/projects/spring-modulith
Apache License 2.0
807 stars 139 forks source link

Reactive Spring Modulith #174

Open nkolosnjaji opened 1 year ago

nkolosnjaji commented 1 year ago

Not sure for JPA, but it would be nice to have reactive JDBC and Mongo starters.

odrotbohm commented 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.

vincentditlevinz commented 1 year ago

Hmm, actually making reactive and non reactive JDBC working together in the same project is not straightforward. Finally I gave up and use a simple @EventListener for the moment (you can see this here)

So having a Reactive implementation would be a great addition :+1:

pcuriel commented 1 year ago

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.

vincentditlevinz commented 1 year ago

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.

denniseffing commented 1 year ago

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

denniseffing commented 1 year ago

@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, the DataSourceTransactionManager has a @ConditionalOnMissingBean(TransactionManager.class). This means that the bean is only auto-configured, if the ReactiveTransactionManager was not auto-configured beforehand.

The R2dbcTransactionManagerAutoConfiguration is less restrictive because the R2dbcTransactionManager has a @ConditionalOnMissingBean(ReactiveTransactionManager.class). This means it is auto-configured even if a PlatformTransactionManager was auto-configured beforehand.

Spring Modulith Events MongoDB auto-configures a MongoTransactionManager which is a PlatformTransactionManager 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 a R2dbcTransactionManager 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:

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.

denniseffing commented 1 year ago

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:

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 😇

odrotbohm commented 1 year ago

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.