Don't Drink Too Much Reactive Cool Aid
Jan Wedel
Posted on June 23, 2018
Cross-posting of my blog article.
In the last years, the idea of "reactive server applications" was being promoted heavily, especially by new frameworks. In the Java world, it's been RxJava, vert.x and recently Spring WebFlux.
While generally liking the underlying ideas, I find the way that it's beeing pushed both by the writer as well as users problematic. After a conversation I had on twitter with Josh Long, I'd like to explain why.
There are a couple of myths around it, most dominantly...
Synchronous code is slow / Reactive code is fast
Is that true?
So first, what does synchronous mean? In the early days, code was executed on a computer in a sequential manner. Instruction by instruction until the code ended. There came the need to run more than one program at a time. This created the need for processes. You can now have multiple programs that run concurrently, not necessarily in parallel. The Processor would simply switch back and forth between the program execution and keep memory separated.
Then, there was the need to do things within one program concurrently. Threads were created that allow to run multiple parts of a program concurrently but allowing shared memory.
Synchronous code would simply run in one process and one dedicated thread until its done.
A reactive programming model assumes a central handling of all things that go in and out of the program (I/O). One thread would typically be doing multiple things, looping through possible things that could happen. Is there a new TCP connection? Is my byte written to a stream? Is the file loaded from disk? Once it's done, it will inform some registered piece of code that it done. This would then be a call-back function.
So, is this synchronous code slow to reactive code? In fact synchronous code is faster because the thread is not used to do different things in between. Like when you're currently writing to a TCP stream, nothing interrupts sending out bytes because some other part of the program wants to read from a file. So assuming an ideal pipe (like infinite I/O throughput), synchronous code would be faster. But most of the time, I/O is significantly slower than computation meaning there is plenty of time to do other stuff while waiting on I/O to complete.
Threads Are Expensive
Now, you have the possibility of using your single thread to do other stuff in the meantime. Why is that an issue? Well, to create a thread, it takes some kilobytes of RAM and switching context between multiple threads is expensive as well.
See here for example. At some point, it eats up all of your memory and CPU.
Reactive Scales Better
In classic web servers, for each incoming web request, a new thread was either started or reused from a pool, and then all the handling, DB calls etc. was done in that thread. That's still no problem until you get higher loads. Because of the RAM usage and some OS limits, you cannot have an infinite number of threads. The limit is probably around a couple of thousand threads. So either you're application goes out of memory (no thread limit enforced) or it gets slower processing responses (limited thread pool) because some requests have to wait until a used thread is freed.
The thing is, to actually hit this limit, you must be some of a lucky person. Even a single instance of a Spring Boot application e.g. can serve a considerable high load with extremely slow response times.
When you hit that limit, you can start to just spawn multiple instances of the application (if it's able to) and use a load balancer.
To put it in another way: A reactive app might have slower response times on low load, but it is able to keep this response time constant for a higher number of incoming requests. It is able to use the existing resources of your server more efficiently.
This is illustrated in a couple of posts you can find here and here.
The Programming Model
While it seems to be a good idea to use asynchronous I/O handling, despite the fact that is not "better" in all scenarios, there is one serious flaw: It completely changes the way you need to write your code. You cannot just "switch on" async in most popular programming languages and good to go.
As I mentioned before, there is the notion of call-backs. So conceptually, instead of doing
someFunction() {
value = getValue()
print(value)
}
you need to do something like this:
someFunction() {
getAsyncValue(callback)
}
callback(myValue) {
print(myValue);
}
But, this is just the very basic case. It get's worse when you have nested calls..
someFunction() {
getAsyncValue(valueCallback)
}
valueCallback(myValue) {
getUserFromDbAsync(dbCallBack)
}
dbCallback(user) {
print(user + myValue);
}
This is a combination of two calls, and oops, how to I pass myValue
to the dbCallback
function?
Now I can use lambdas and closures to freeze floating values inside the code:
someFunction() {
getAsyncValue(myValue -> {
getUserFromDbAsync(user -> {
print(user + myValue)
})
})
}
Is that better? And what about error handling? Easy:
someFunction() {
getAsyncValue(myValue -> {
getUserFromDbAsync(user -> {
print(user + myValue)
}).onError(() -> {
print("can't get user")
}
}).onError(() -> {
print("can't get value")
})
}
What if I need to handle the DB user error in my getAsyncValue
callback? Have a look at my vert.x 2 review article. It's old but it covers some ideas of this article.
With reactive stacks like WebFlux, you can use chained pseudo-functional calls like:
someFunction() {
getAsyncValue()
.map(myValue -> print);
}
However, it then gets tricky when you want to combine the calls above and add error handle. You have a long chain of .map
, flatMap
and zip
that won't improve things a lot compared to the async version and are bad compared to the synchronous version. Have a look at the examples here. Actually, one method call-chain is spread across 12 lines, including pseudo if-else calls.
In all of the above cases, using function references, lambdas or reactive extensions, the code suffers in:
-
Readability
- It's obvious to see that the code is longer
- It's more complex
- It doesn't have a sequential flow anymore. Or to put it that way: The code is executed in another order as it is written down in the sources.
-
Maintainability
- It's hard to add functionality
- Debugging this code is much more complicated. You cannot simply step through it
-
Testability
- You cannot simply mock 1 or 2 classes and call a method. You need to created objects that behave synchronously but are actually asynchronous.
-
Code Quality
- Because it is hard to digest the code by reading, it will have more bugs than synchronous code.
-
Development Speed
- Very simple tasks like e.g. two REST requests with different return types that need build up on each other (e.g. get master data, enrich with some related data and return merge to UI)
- Simply put: You cannot just write the way you think
I've written professional applications with vert.x, RxJava and most recently Spring WebFlux and the above issues could be found in all of them in different flavors.
What Now?
What you actually want is code that you can write and read as synchronous but that is executed by the underling runtime in an asynchronous way that support conventional error handling:
someFunction() {
try {
myValue = getAsyncValue()
user = getUserFromDbAsync()
print(user + myValue)
} catch (ComputationException e) {
print("can't get value")
} catch (DbException e) {
print("can't get user")
}
Now, you'd have the good path within three lines and separated error handling. The code is very easy to understand. What happens is, that the runtime will park the execution of getUserFromDbAsync
until the DB returns the data. In the meantime, the thread would be used for other tasks.
Erlang implements that within it's Actor model and BEAM VM. When you read from a file, you just call io:get_line
and get the result. The VM will suspend the current actor and it's Erlang process (which is a light weight thread that takes only a few bytes of memory).
For the JVM, there is currently Project Loom that tries to implement continuations in the JVM and an abstraction called Fiber as part of the JDK. To me, it looks promising, but it will take some time and it's not 100% sure that it will ever be part of Java.
Python has asyncio that provides some more language features more advanced than Java but still, it's too exposed, IMHO.
Conclusion
With my article, I'd like to emphasize the point of reactive programming not being the silver bullet (as it is most often the case). Be sure to know what you're doing and precisely why you need it.
Taken from my discussion with Josh:
It think that there are currently only a handful scenarios that would require to build fully reactive server applications. Otherwise, just stick with the "classic" approach.
The problem is, after you were writing applications with your reactive hammer for some time (which has a steep learning curve), you see every task as a nail. This would be OK if code quality doesn't suffer but it objectively does .
And, to inexperienced developers, it might seem cool because it's so hard to understand. As if only the best developers will understand it. But trust me, this is not the case. The best language and API designs are those, that are easy to understand, write and read.
Posted on June 23, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.