visit
Spring WebFlux is a reactive, non-blocking web framework for building modern, scalable web applications in Java. It is a part of the Spring Framework, and it uses the Reactor library for implementing reactive programming in Java.
The term, “reactive,” refers to programming models that are built around reacting to change — network components reacting to I/O events, UI controllers reacting to mouse events, and others. In that sense, non-blocking is reactive, because, instead of being blocked, we are now in the mode of reacting to notifications as operations complete or data becomes available.
In Spring WebFlux (and non-blocking servers in general), it is assumed that applications do not block. Therefore, non-blocking servers use a small, fixed-size thread pool (event loop workers) to handle requests.
While WebFlux request processing is slightly different:
We need a pretty minimalistic app generated by . The code is available in the .
All thread-related topics are very CPU-dependent. Usually, number of processing threads that handle requests is related to the number of CPU cores. For educational purposes you can easily manipulate count of threads in a pool by limiting CPUs when running Docker container:
docker run --cpus=1 -d --rm --name webflux-threading -p 8081:8080 local/webflux-threading
Our app is a simple fortune teller. By calling /karma
endpoint you will get 5 records with balanceAdjustment
. Each adjustment is an integer number that represents a karma given to you. Yes, we are very generous because the app generates only positive numbers. No bad luck anymore!
@GetMapping("/karma")
public Flux<Karma> karma() {
return prepareKarma()
.map(Karma::new)
.log();
}
private Flux<Integer> prepareKarma() {
Random random = new Random();
return Flux.fromStream(
Stream.generate(() -> random.nextInt(10))
.limit(5));
}
log
method is a crucial thing here. It observes all Reactive Streams signals and traces them into logs under INFO level.
Logs output on curl localhost:8081/karma
is the following:
As we can see, processing is happening on the IO thread pool. Thread name ctor-http-nio-2
stands for reactor-http-nio-2
. Tasks were executed immediately on a thread that submitted them. Reactor didn’t see any instructions to schedule them on another pool.
@GetMapping("/delayedKarma")
public Flux<Karma> delayedKarma() {
return karma()
.delayElements(Duration.ofMillis(100));
}
We don’t need to add log
method here because it was already declared in the original karma()
call.
This time only the very first element was received on the IO thread reactor-http-nio-4
. Processing of the rest 4 were dedicated to a parallel
thread pool.
Javadoc of delayElements
confirms this:
Signals are delayed and continue on the parallel default Scheduler
You can achieve the same effect without delay by specifying .subscribeOn(Schedulers.parallel())
anywhere in the call chain.
Using parallel
scheduler can improve performance and scalability by allowing multiple tasks to be executed simultaneously on different threads, which can better utilize CPU resources and handle a large number of concurrent requests.
However, it can also increase code complexity and memory usage, and potentially lead to thread pool exhaustion if the maximum number of worker threads is exceeded. Therefore, the decision to use parallel
thread pool should be based on the specific requirements and trade-offs of the application.
We are going to use a flatMap
and make a fortune teller more fair. For each Karma instance it will multiply original adjustment by 10 and generate the opposite adjustments, effectively creating a balanced transaction that compensates for the original one.
@GetMapping("/fairKarma")
public Flux<Karma> fairKarma() {
return delayedKarma()
.flatMap(this::makeFair);
}
private Flux<Karma> makeFair(Karma original) {
return Flux.just(new Karma(original.balanceAdjustment() * 10),
new Karma(original.balanceAdjustment() * -10))
.subscribeOn(Schedulers.boundedElastic())
.log();
}
As you see, makeFair’s
Flux should be subscribed to a boundedElastic
thread pool. Let’s check what we have in logs for the first two Karmas:
Reactor subscribes first element with balanceAdjustment=9
on IO thread
Then boundedElastic
pool works on Karma fairness by emitting 90
and -90
adjustments on boundedElastic-1
thread
Elements after the first one are subscribed on parallel thread pool (because we still have delayedElements
in the chain)
boundedElastic
scheduler?
By default, the boundedElastic
thread pool a maximum size of the number of available processors multiplied by 10, but you can configure it to use a different maximum size if needed
By using an asynchronous thread pool like boundedElastic
, you can offload tasks to separate threads and free up the main thread to handle other requests. The bounded nature of the thread pool can prevent thread starvation and excessive resource usage, while the elasticity of the pool allows it to adjust the number of worker threads dynamically based on the workload.
single
: This is a single-threaded, serialized execution context that is designed for synchronous execution. It is useful when you need to ensure that a task is executed in order and that no two tasks are executed concurrently.
immediate
: This is a trivial, no-op implementation of a scheduler that immediately executes tasks on the calling thread without any thread switching.