Issue
The scenario: In my Spring cloud gateway I need to modify the request based on some data persisted in my database. Therefore I am implementing a gateway-filter. Consider the following implementations:
Repository Interface:
public interface MyReactiveRepository{
Mono<String> getSomeData();
}
Repository implementation:
@Repository
public class MyReactiveRepositoryImpl implements MyReactiveRepository
{
private final JdbcTemplate template;
public MyReactiveRepositoryImpl(@Autowired JdbcTemplate template){
this.template = template;
}
@Override
public Mono<String> getSomeData(){
return Mono.fromCallable(
// Wrapping blocking code in a Mono:
() -> template.queryForObject(SOME_SQL_QUERY, String.class)
).subscribeOn(Schedulers.boundedElastic());
}
}
And the filter:
@Component
public class MyGatewayFilter implements GatewayFilter
{
private final MyReactiveRepository repository;
public MyGatewayFilter(@Autowired MyReactiveRepository repository){
this.repository = repository;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){
return repository.getSomeData()
.flatMap(
(repositoryData) -> {
// Modify the exchange based on the repository data in some way...
// Then return
return chain.filter(exchange);
}
);
}
}
The key here is that I have the blocking JDBCTemplate which I am "attempting" to make non-blocking as to preserve the non-blocking nature of the spring gateway. Naturally, I could make use of Spring R2DBC, but by Googling I have gotten the impression that the"Mono.fromCallable+BoundedElastic-scheduler"-trick also will work.
My question is whether or not my implementation actually will have the same performance compared to an implementation making use of R2DBC: What are the disadvantages of my implementation?
Thank you
Edit: I first saw the "fromCallable" pattern at the project reactor FAQ so I would assume that there are scenarios where it is acceptable?
Solution
I have used both approaches in production, wrapped JDBC calls and pure R2DBC. They were different applications so I won't speak to the performance because there are other parameters that affect it a lot, e.g. existence of ORM.
Your approach is mostly correct but it does have a few issues. The first one is that you are currently "leaking" the scheduler outside of your repository, which means that any stream that connects to it will now run your code on the bounded elastic scheduler that you want to use for your DB calls. This will eventually lead to a nasty deadlock. Make sure to define an output scheduler (parallel or single) and transfer execution there before leaving the repository.
The other issue I see here is that your implementation will not play well with retry and repeat mechanisms. Using any of those would not cause a DB call to be made but instead, use the data that was fetched previously. Use Mono.defer
to fix that:
public Mono<String> getSomeData(){
return Mono.defer(() -> Mono.fromCallable(
() -> template.queryForObject(SOME_SQL_QUERY, String.class)))
.subscribeOn(Schedulers.boundedElastic())
.publishOn(Schedulers.parallel());
}
Using the default schedulers is fine for small applications but for more involved projects it would be better to have a dedicated DB bounded elastic and a parameterized output scheduler.
The last issue, which was a pain to figure out, is that your JDBC threadpool should be the same size as the boundedElastic used for the db calls. Otherwise some calls "go missing" under a lot of stress.
I hope this helps!
Answered By - Khepu
Answer Checked By - Clifford M. (JavaFixing Volunteer)