Our journey with Reactor

Gautier DI FOLCO
Linagora Engineering
5 min readJan 29, 2019

James is an Open Source Mail Server. Our current version (3.2.0) has a default configuration relying on Cassandra.

In order to deal with Cassandra we have set up a class in charge of that:

It’s pretty straight-forward: you give a Statement and you obtain a CompletableFuture. Given that you will combine your CompletableFuture in order to get a big one solving your problem:

Things become nasty when you have nested types:

Here, the goal is to transform a Stream<CompletableFuture<Optional<Attachment>>> to a CompletableFuture<Stream<Attachement>>.

In order to achieve that, James have been tooled with two classes: CompletableFutureUtils and FluentFutureStream, to help us to improve composition.

The real issue with these classes is that it’s not the aim of a Mail Server to fix language/libraries weaknesses.

Our approach had three major drawbacks:

  • It had a deep impact on our design: in order to compose better with our abstractions, a huge part of some classes have been designed around them and were not only dealing with the problem they were supposed to solve.
  • Concurrency issues: CompletableFuture represents a running code and not a runnable code, consequently, when you have a Stream<CompletableFuture<?>> on which you map a method deal also with Stream<CompletableFuture<?>>, there is an explosion of running things and the JVM spends a lot of its time to context switch. We also had a threads management issue: most of our business code were running in Cassandra driver threads.
  • Code complexity: the code was very complex, even well tested, it remains hard to understand at first eye sight.

The decision have been made to use a dedicated tool which does this job far better than us: Reactor. Here is what we have learned during our migration to Reactor.

The good

Reactor is really simple on type-level: two classes (Publisher) are exposed: Mono (which represents 0–1 value) and Flux (which represents 0-n values).

At the time of 3.2.5.RELEASE:

  • Mono has more or less 180 methods
  • Flux has more or less 375 methods

It means that there is a method for everything.

The glorious Mono.retry

We used to have a dedicated mechanism to handle retries:

It was the goal of FunctionRunnerWithRetry to deal with all the tediousness of retry behavior, while it only takes two lines with Reactor:

It works with more or less every Mono, additionally it can deals with back-offs, which will reduce the pressure if needed.

Easy composition

It also composes greatly, you can easily go from a Mono to a Flux, and conversely, you do not have to deal the presence or the absence of value, it's already done underneath.

Finally, the declarative way of building Publishers makes the code really neat:

null-free

The great point of Reactor is its ability to not deal with null. First Mono and Flux are perfectly aware of emptiness. Secondly, if you try to insert a null in the steps, everything stops:

The bad

The first big problem comes from the number of methods: you can do a lot of things, but since you have only a few types, it’s very hard to find what you want easily.

The second one is the lack of information at type-level.

When you have a Mono<A> and you want to get a A, you have basically two ways: either you are sure that you will get a value (thanks to switchIfEmpty for example) and you can safely call block. Or you are not sure and you have to use a blockOptional which returns a Optional<A>.

This case can be solved with a simple test, sadly there is worse: all parallel by default on Flux, look at this code:

This code is great, straightforward, nothing to drop, and, most importantly, dangerously parallel. By default this Flux creates a huge number of parallel Mono, it works until the connections pool is exhausted, which makes this code fail.

Thankfully, as said earlier, there is a method for everything:

The differences with CompletableFuture

Basically, it is not an issue, but for a migration, you have to take care of this:

  • CompletableFuture.thenApply becomes Mono.map
  • CompletableFuture.thenCombine becomes Mono.zip
  • CompletableFuture.thenCompose becomes Mono.flatMap

Coming from a strong FP background, the Reactor naming is more pleasant.

There is meanwhile a very important difference to take care of: the Void. While CompletableFuture<Void>.thenApply (and others) works well, giving a null to the called method; Mono<Void>.map (and flatMap) never triggers the given method.

It clearly makes sense: how to give a value you can not instantiate? But on an other hand it’s destabilizing.

To continue an ongoing chain of operations you have to use Mono.then or Mono.thenEmpty

Increase concurrency conflicts

We all know that random bug which happens 7% of the time, under very-specific circumstances, well, now it appears 80% of the time.

That is the real thing with Reactor: it not only reduces the memory and time consumption of your application, but it also reduce the pressure of it on the outer world.

While your application was previously slow enough to avoid a bug most of the time, now, you can not escape: you have to fix it, for your own good.

The ugly

There is a truth everyone need to hear: all Publishers are not born equal.

From time to time, Mono are directly created from CompletableFuture (and it was the case for all our Cassandra operations), which means that they are already running, let’s look at this piece of code:

Here, we can expect tryInsertModSeq to just declare a Publisher which will be run if and only if the previous one is empty, in fact, it is already running.

There is a last trap with Mono.fromFuture: you can not replay them, so this code:

Will only be tried once if there is a Mono.fromFuture at its root because it can not be replayed. Thanks to Mono.defer we are able to make Suppliers of them and to act as any other part of the Publisher.

Conclusion

Reactor is a great tool where every details have been intensively thought of, which makes it a nice solution to our problem.

On an other hand, most of the pitfalls I have listed would have been avoided with a finer type-level.

Note: Most of the code examples come from the James code base and are de facto licensed under the Apache License, Version 2.0.

--

--