Reactor migration mistakes: usual suspects

Gautier DI FOLCO
Linagora Engineering
3 min readMay 28, 2020

One year and a half ago we made the decision to leverage reactive streams and we have published our recent conversion experience.

Since then, we are regularly moving blocking code (aka legacy code) to reactive code. These changes almost every time involve deep architectural modifications and small mistakes hard to spot.

As developers we tend to dislike repetition and easily spot patterns. Doing the same conversion over and over again, it happens that we often do the same mistakes, even if at first sight they do not seem to.

This post is intended to, for common symptoms, give a checklist to perform in order to get things working. We have taken the point of view of a functional refactoring (the functionality should not change, but the technical details have).

An action is not performed anymore

It usually happens when a method signature changes.
If you are lucky, it is direct, for example :

Changes to the reactive version:

which breaks the following code:

It can easily be solved with:

That was the easy cases, sometimes however, changes have an indirect impact.
For example, if instead of relying on a Mono, which can be either just or empty:

you have changed it to an assertion relying on Mono.error:

it breaks the following code:

The solution is to use then:

An action fails or happens too intensively

If you are really unlucky you have some temporal coupling.

It means that the actions have to be performed in a certain order or at a specific pace.
The first case looks genuine:

That way, even if you think you have plain Publishers, you have what I call fake Publishers, which are worse than Hot Publishers, since they represents nothing more than a past action’s result.

While the following code is supposed to work, it will actually throw at Publisher’s creation (line 1):

Remark: it is a toy example, keep in mind that it happens at module scale.
If the two first lines are coincidentally in the right order, you will have troubles if you try to leverage other Reactor operations.
The correct step to take is to make them Cold Publishers:

You can also have ordering issues with Flux:

Do not expect result to be {1, 2, 3, 4}. While Flux is sequential, flatMap executes things in parallel.

It the good case (as above), you do not have any retry and you will see the failure.In the nasty case you will generate more queries than necessary.

There are three way to get out of it:
* You want a more limited concurrency, then use the flatMap overloaded with the concurrency level
* You do not mind about the execution order: use flatMapSequential
* The execution order is important: use concatMap

An action blocks or takes a long time

It happens when the Scheduler where your code is running does not have enough workers.

By default (ie. you have never called publishOn, subscribeOn, nor runOn during our Publisher building) is Schedulers.immediate().

You end up in deadlock if your code requires more workers than your Scheduler possesses (by default, more than one).
It is solvable by using one of the following Schedulers, either in a subscribeOn at the end of the Publisher or in publishOn during Publisher’s construction:
* From blocking code: boundedElastic or elastic
* From non-blocking code: parallel

Conclusion

While being far to be complete, this article aims to cover the most frequent introduced mistakes during migration to Reactor.

--

--