Blog

Don't block the Event Loop (WebFlux ft. Node.js)

Tú WFH tiredly wrote

Just a few of my little notes when working with hilarious Event Loops.

Spring WebFlux and Node.js use the same concurrency model based on an event-loop.

Difference is a Node.js (v10 and earlier versions) instance can only utilize 1 event loop thread, while WebFlux (Netty based) can utilize 1 event loop thread pool (there’ll be several event loop threads based on total CPU-cores) and can spawn many others worker threads.

Node.js (<v10)

When using Node.js, performing CPU-intensive (or blocking) tasks in Node.js could become a nightmare, because we have to execute the blocking task in the separated instance, and then we also need to maintain a message-queue (like Redis/RabbitMQ) to exchange data between these two instances - which will increase the complex of the application and more code to write/maintain.

Otherwise, if one request executes the blocking task, it will block all other request - which called “block the event-loop”, and heavily drop the throughput.

Spring WebFlux

While in WebFlux, we can perform blocking code in a worker thread by using thread pool abstractions provided, called Schedulers, to use with the publishOn operator that is used to switch processing to a different thread pool.

@GetMapping
public Mono<Response> getResponse() {
  return Mono.fromCallable(() -> blockingOp())
    .publishOn(Schedulers.elastic());        
}

And then we can perform the CPU-intensive/blocking tasks without the pain to use the addition instance or maintaining the MQ.

Similar to Node.js, if we call blocking libraries (like JDBC blocking call) without scheduling that work on a specific scheduler (which means not use the .publishOn), those calls will block one of the few threads available (the Netty event loop thread pool) and then the application will only be able to serve a few requests concurrently, and that might drop the throughput as well.

(I’ve heard that on Node.js 12 and later we can utilize the same worker thread model as WebFlux on Node.js 12’s Worker Thread, but I’ve never work on Node.js Worker Thread so…)

How to use Blocking Libraries in WebFlux

On Spring FAQ, they said:

One suggestion for handling a mix of blocking and non-blocking code would be to use the power of a microservice boundary to separate the blocking backend datastore code from the non blocking front-end API. Alternatively, you may also go with a worker thread pool for blocking operations, keeping the main event loop non-blocking that way.

That you should:

  • Use usual blocking return types when you’re dealing with a blocking library, like JDBC.
  • Use Mono<> and Flux<> return types when you’re not tied to such libraries.

Of course this won’t be non-blocking at all, but you’ll be able to do more work in parallel without dealing with the complexity.

Tired of Event Loops, CallBacks and Reactive?

Synchronous code is easier to write, test, debug, maintain, and understand than async code. The only reason to write async code is that threads are so expensive.

The future for scalable Java code is fibers, not async

Then it’s time to adopt the “Green Thread” threading model, there’re two candidates for that approach: Go and JVM’s Fiber (Project Loom). emoji-smirk

Wikipedia defines green threads as “threads that are scheduled by a virtual machine (VM) / runtime instead of natively by the underlying operating system”. Green threads emulate multi-threaded environments without relying on any native OS capabilities, and they are managed in user space instead of kernel space, enabling them to work in environments that do not have native thread support.

With Green Threads, your code will still stay with imperative style while you can earn the performance-wise, high throughput benefits with NIO. No callback hell, or tons of function chaining to worry about. emoji-smiley

Go has done a very great job with its concurrency model: Goroutine and Channel.

Also, Loom seems to be a cross-cutting changes on JVM implementation, to change imperative blocking APIs to use the “ForkJoinPool” On Fiber virtual thread context.

Project Loom will add fibers, very lightweight threads, to Java. Fibers are so light weight that it is completely practical to spin up as many as you need. So just write first semester CS synchronous code and and execute it on a dedicated fiber. This is much easier than writing async code to do the same task. Synchronous code will use the same JDBC we all know and love. No need to learn a new API (like Reactive). Existing code can be made to work with few if any changes.

Unfortunately, I can only adopt these great and powerful things in my side-projects. emoji-sob