The rubric isn't dead! I just had to put it into "stasis" because of a very-low-latency but high-cognitive-cost event (and not only cognitive π ): buying a house! But now that I'm back to being stable, let's restart with a technical deep dive into a silent killer of reactive programming!
π Deep dive: anatomy of an (invisible) "Hot Loop"
A few days ago, on the project I'm assigned to, I watched my Mac turn into a little heater. The culprit? A "creative" handling of a Mono.empty().
Imagine a DB query that finds nothing and returns an empty Mono. The .repeat() sees that the flow has ended and re-subscribes immediately. The cycle repeats at the speed of light because there's no latency between one completion and the new subscription.
π The numbers of the disaster
The test below reproduces exactly that scenario: four flatMaps in parallel on a Mono.empty() with .repeat(), for one second.
@Test
void test_repeatOnEmptyCausesHotLoop() {
final AtomicInteger subscriptions = new AtomicInteger();
final AtomicInteger onNext = new AtomicInteger();
final Set<String> threads = ConcurrentHashMap.newKeySet();
final String actual = Flux.range(0, 4)
.flatMap(i -> Mono.<String>empty()
.doOnSubscribe(sub -> {
subscriptions.incrementAndGet();
threads.add(Thread.currentThread().getName());
})
.doOnNext(v -> onNext.incrementAndGet())
.repeat())
.subscribeOn(Schedulers.parallel())
.take(Duration.ofMillis(1000))
.blockLast();
assertNull(actual);
System.out.println("Subscriptions in 1000ms (1s): " + subscriptions.get());
System.out.println("onNext signals: " + onNext.get());
System.out.println("Threads: " + threads);
assertEquals(0, onNext.get(), "onNext signals");
}
Output:
Subscriptions in 1000ms (1s): 16059951
onNext signals: 0
Threads: [parallel-4, parallel-2, parallel-8, parallel-6]
In a single second, on 4 parallel threads, Project Reactor handled over 16 million subscriptions. The result? Zero elements produced (onNext signals: 0), but CPU at 100%.
π The cost of "silence"
The real danger here is that there are no Exceptions. Nothing crashes. The system seems "just" very busy, while in reality it's running into a wall at a crazy speed.
If you use reactivity to handle retries or loops, always remember: if the starting point is empty, repeat() risks becoming a Hot Loop. A backoff (for example repeatWhen(...)) or the correct handling of a not-found can save you from a self-inflicted DoS attack.
Hoping another 3 months don't go by, see you at the next pill! β