La rubrica non è morta! L'ho solo dovuta mettere in "stasi" a causa di un evento a bassissima latenza ma ad alto costo cognitivo (e non solo cognitivo 😅): comprare casa! Ma ora che so' tornato stabile, ripartiamo con un approfondimento tecnico su un killer silenzioso della programmazione reattiva!
📌 Deep dive: anatomia di un "Hot Loop" (invisibile)
Qualche giorno fa, sul progetto su cui sono allocato, ho visto il mio Mac trasformarsi in una stufetta. Il colpevole? Una gestione "creativa" di un Mono.empty().
Immaginate una query a DB che non trova nulla e restituisce un Mono vuoto. Il .repeat() vede che il flusso è finito e si risottoscrive subito. Il ciclo si ripete alla velocità della luce perché non c'è latenza tra un completamento e la nuova sottoscrizione.
📌 I numeri del disastro
Il test qui sotto riproduce esattamente lo scenario: quattro flatMap in parallelo su un Mono.empty() con .repeat(), per un secondo.
@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 un solo secondo, su 4 thread paralleli, Project Reactor ha gestito oltre 16 milioni di sottoscrizioni. Il risultato? Zero elementi prodotti (onNext signals: 0), ma CPU al 100%.
📌 Il costo del "silenzio"
Il vero pericolo qui è che non ci sono Exception. Non crasha nulla. Il sistema sembra "solo" molto impegnato, mentre in realtà sta correndo contro un muro a velocità folle.
Se usate la reattività per gestire dei retry o dei loop, ricordatevi sempre: se il punto di partenza è vuoto, repeat() rischia di diventare un Hot Loop. Un backoff (per esempio repeatWhen(...)) o la corretta gestione di un not-found possono salvarvi da un attacco DoS auto-inflitto.
Sperando non passino altri 3 mesi, ci vediamo alla prossima pill! ☕