reactor-core icon indicating copy to clipboard operation
reactor-core copied to clipboard

MonoThen creates deep stack traces leading to StackOverflowError

Open nelapsi13 opened this issue 3 years ago • 3 comments

Expected behavior

some kind of error?

Actual behavior

block() never finishes

Steps to reproduce

following code

  1. causes java.lang.StackOverflowError (which is I guess expected according to https://github.com/reactor/reactor-core/issues/1441)
  2. then blocks forever (which doesn't seem expected)
package org.example;

import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;

public class App {
    static AtomicInteger depth = new AtomicInteger(100000);

    private static Mono<Integer> source() {
        return Mono.fromSupplier(() -> {
            var i = depth.decrementAndGet();
            return  i > 0 ? null : 1;
        });
    }

    static Mono<Integer> waitForData() {
        return source()
            .switchIfEmpty(Mono.defer(() ->
//                Mono.just(1).delayElement(Duration.ofNanos(1))
                Mono.delay(Duration.ofNanos(1))
                    .then(Mono.defer(App::waitForData))));
    }

    public static void main(String[] args) {
        waitForData().block();
    }
}

nelapsi13 avatar Jun 29 '22 14:06 nelapsi13

MonoThen recursion-related bug. Can have a fix similar to recursion protection in reactor.core.publisher.FluxConcatArray#ConcatArraySubscriber

OlegDokuka avatar Jul 04 '22 10:07 OlegDokuka

not quite sure about " Can have a fix similar to recursion protection in reactor.core.publisher.FluxConcatArray#ConcatArraySubscriber".

wil it fix never-finishing block() call?

nelapsi13 avatar Jul 04 '22 13:07 nelapsi13

@nelapsi13. Yes, basically your code creates too deep stack trace so at some point any call that the reactor tries to do ends with Overflowexception which can not be handled. However, if we apply a trick, we can have that stack trace be in constant size, so an exception will not throw and the result wich unblock a lock will be delivered

OlegDokuka avatar Jul 07 '22 08:07 OlegDokuka

As a matter of fact, the case is different from reactor.core.publisher.FluxConcatArray. The code is creating a recursion, for which we don't have a way to overcome. The subscription flow is forming a recursive object hierarchy:

block (0) -> waitForData (1) -> switchIfEmpty (2) -> defer (3) -> then (4) -> defer (5) -> waitForData (1') -> ...

As (1) and (1') are different objects, there is no way of determining the recursion and thinking about any internal optimisation. Consider structuring the reactive chain differently, as this type of generator is doomed to cause StackOverflowError.

Perhaps, instead of:

waitForData().block();

this could do the trick instead:

Flux.from(source()).single().retry().block();

chemicL avatar Aug 18 '22 11:08 chemicL

StackOverflowError is not a problem (more or less) the problem is that block() never finishes (i.e. blocks forever)

nelapsi13 avatar Aug 18 '22 12:08 nelapsi13

The reactive chain is unable to terminate block on such an occasion, as when a SOE happens, the terminal signal can't be delivered to block's subscriber. All in all, JVM VirtualMachineErrors (https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/VirtualMachineError.html) are not considered recoverable. Also, please note in the latest version the logging of such erroneous states has been added: https://github.com/reactor/reactor-core/pull/3122

chemicL avatar Aug 18 '22 12:08 chemicL

ok, thanks for clarification

nelapsi13 avatar Aug 18 '22 13:08 nelapsi13