Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Behaviour of doFinally #951

Closed
LukasHavemann opened this issue Nov 15, 2017 · 9 comments
Closed

Behaviour of doFinally #951

LukasHavemann opened this issue Nov 15, 2017 · 9 comments
Labels
type/bug A general bug
Milestone

Comments

@LukasHavemann
Copy link

LukasHavemann commented Nov 15, 2017

Expected behavior

@Test
public void errorBeforeFinallyTest() {
    System.err.println("> error before finally");
    Mono.just(true)
        .map(this::throwError)
        .doFinally(this::doFinalThing)
        .subscribe();
}

@Test
public void errorAfterFinallyTest() {
    System.err.println("> error after finally");
    Mono.just(true)
        .doFinally(this::doFinalThing)
        .map(this::throwError)
        .subscribe();
}

private void doFinalThing(SignalType any) {
    System.err.println("do finally called");
}

private Boolean throwError(Boolean x) {
    System.err.println("throw exception");
    throw new IllegalStateException();
}

According to the javadoc shouldn't be both tests have the same log output?

Actual behavior

> error after finally
throw exception
do finally called


> error before finally
throw exception

Steps to reproduce

See code above

Reactor Core version

3.1.1

JVM version (e.g. java -version)

java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)

OS version (e.g. uname -a)

MINGW64_NT-10.0

@simonbasle simonbasle added the for/stackoverflow Questions are best asked on SO or Gitter label Nov 15, 2017
@simonbasle
Copy link
Member

simonbasle commented Nov 15, 2017

That's not what I see, the second example goes through doFinalThing as well, despite the error being downstream of doFinally. That's because the error in the map will trigger a cancellation upstream, which is seen by doFinally: you can make that obvious by appending the any variable to the printout.

Modifying doFinalThing accordingly and slightly amending your code to have the subscribe swallow the exception, I have made a quick test that performs both tasks:

@Test
public void errorBeforeAndAfterFinallyTest() {
	System.err.println("> error before finally");
	Mono.just(true)
	    .map(this::throwError)
	    .doFinally(this::doFinalThing)
	    .subscribe(v -> {}, e -> {});

	System.err.println("> error after finally");
	Mono.just(true)
	    .doFinally(this::doFinalThing)
	    .map(this::throwError)
	    .subscribe(v -> {}, e -> {});
}

I get the following output:

> error before finally
throw exception
do finally called with onError
> error after finally
throw exception
do finally called with cancel

@LukasHavemann
Copy link
Author

Thank you for the clarification!

@baptistemesta
Copy link
Contributor

Hi, I'm commenting in that because I think there is still an issue on the doFinally.

    @Test
    public void withoutConsumerInSubscribe() {
        System.err.println("> subscribe without consumer: map_doOnError_doFinally");
        Mono.just(true)
                .map(this::throwError)
                .doOnError(e -> System.err.println("doOnError"))
                .doFinally(any -> System.err.println("doFinally " + any.toString()))
                .subscribe();
        System.err.println("> subscribe without consumer: doFinally_map_doOnError");
        Mono.just(true)
                .doFinally(any -> System.err.println("doFinally " + any.toString()))
                .map(this::throwError)
                .doOnError(e -> System.err.println("doOnError"))
                .subscribe();
        System.err.println("> subscribe without consumer:  map_doFinally_doOnError");
        Mono.just(true)
                .map(this::throwError)
                .doFinally(any -> System.err.println("doFinally " + any.toString()))
                .doOnError(e -> System.err.println("doOnError"))
                .subscribe();
    }

    @Test
    public void withConsumerInSubscribe() {
        System.err.println("> subscribe with consumer: map_doOnError_doFinally");
        Mono.just(true)
                .map(this::throwError)
                .doOnError(e -> System.err.println("doOnError"))
                .doFinally(any -> System.err.println("doFinally " + any.toString()))
                .subscribe(v -> { }, e -> { });
        System.err.println("> subscribe with consumer: doFinally_map_doOnError");
        Mono.just(true)
                .doFinally(any -> System.err.println("doFinally " + any.toString()))
                .map(this::throwError)
                .doOnError(e -> System.err.println("doOnError"))
                .subscribe(v -> { }, e -> { });
        System.err.println("> subscribe with consumer: map_doFinally_doOnError");
        Mono.just(true)
                .map(this::throwError)
                .doFinally(any -> System.err.println("doFinally " + any.toString()))
                .doOnError(e -> System.err.println("doOnError"))
                .subscribe(v -> { }, e -> { });
    }

    @Test
    public void whithoutDoOnError() {
        System.err.println("> whithoutDoOnError");
        Mono.just(true)
                .map(this::throwError)
                .doFinally(any -> System.err.println("doFinally " + any.toString()))
                .subscribe();
    }

    private Boolean throwError(Boolean x) {
        throw new IllegalStateException();
    }

the produce

> subscribe without consumer: map_doOnError_doFinally
doOnError
> subscribe without consumer: doFinally_map_doOnError
doFinally cancel
doOnError
> subscribe without consumer:  map_doFinally_doOnError
doOnError
doFinally onError
> subscribe with consumer: map_doOnError_doFinally
doOnError
doFinally onError
> subscribe with consumer: doFinally_map_doOnError
doFinally cancel
doOnError
> subscribe with consumer: map_doFinally_doOnError
doOnError
doFinally onError

> whithoutDoOnError
TestFinally > whithoutDoOnError FAILED
    reactor.core.Exceptions$ErrorCallbackNotImplemented
        Caused by: java.lang.IllegalStateException at TestFinally.java:69

java.lang.IllegalStateException
reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException
Caused by: java.lang.IllegalStateException
	at TestFinally.throwError(TestFinally.java:69)
[...]

As you can see, when using .subscribe() instead of .subscribe(consumer, errorConsumer) the doFinally is not executed if it is placed after the doOnError.

Also, I upgraded from 3.0.7 to 3.1.2 and its not possible to write the code from whithoutDoOnError with the subscribe() without parameters.

Reactor Core version

3.1.2

@simonbasle simonbasle reopened this Nov 27, 2017
@simonbasle
Copy link
Member

Thanks for the demonstration @baptistemesta. Not sure what you mean by "it is not possible to write..." (copy-pasted the whole code in a test and it worked fine from a compilation point of view).

Indeed the case where no error handler is provided in subscribe() and the doFinally is closest to the subscribe will skip the handler.

This is due to the subscribe throwing an UnsupportedOperationException complaining about the missing error handler, and the doFinally not being protected against that. In other cases, the doOnError does protect against said UOE, so in effect the doFinally is unaffected.

simonbasle added a commit that referenced this issue Nov 27, 2017
This commit wraps the application of doFinally's handler in a
try/finally block, so that in the case where a final subscriber throws
(typically because no error handler was defined), it still executes the
doFinally handler.
@simonbasle simonbasle added type/bug A general bug and removed for/stackoverflow Questions are best asked on SO or Gitter labels Nov 27, 2017
@simonbasle simonbasle added this to the 3.1.3.RELEASE milestone Nov 27, 2017
@baptistemesta
Copy link
Contributor

That was fast :)
thank you!

@simonbasle
Copy link
Member

well the second repro case was a huge help, so thanks for providing that ;) 👍

@baptistemesta
Copy link
Contributor

baptistemesta commented Nov 28, 2017

BTW when I was saying it is not possible to write I meant the code was not throwing exception on whithoutDoOnError i.e.. the UnsupportedOperationException

@unoexperto
Copy link

unoexperto commented Oct 22, 2022

I just spent 3 hrs trying to debug why I'm getting SignalType.CANCEL on exception thrown upstream. I wish this behavior was cleaner. It's very very confusing right now. I still don't understand why it's happening. Could you please explain it?

Schematically my code looks like this:

Mono.fromCallable {
    error("zzz")
}
    .doFinally { signal ->
       // get CANCEL here
    }
    .subscribe({ event ->
        mainSink.emitNext(event, busyLooping)
    }, { ex ->
        // error
        flushingSink.emitError(ex, busyLooping)
    }, {
        // complete
        flushingSink.emitComplete(busyLooping)
    })

@chemicL
Copy link
Member

chemicL commented Oct 25, 2022

I just spent 3 hrs trying to debug why I'm getting SignalType.CANCEL on exception thrown upstream. I wish this behavior was cleaner. It's very very confusing right now. I still don't understand why it's happening. Could you please explain it?

Schematically my code looks like this:

Mono.fromCallable {
    error("zzz")
}
    .doFinally { signal ->
       // get CANCEL here
    }
    .subscribe({ event ->
        mainSink.emitNext(event, busyLooping)
    }, { ex ->
        // error
        flushingSink.emitError(ex, busyLooping)
    }, {
        // complete
        flushingSink.emitComplete(busyLooping)
    })

@unoexperto Please provide a complete reproducer (that we can run locally) either as an issue if you think you're facing a bug, or as a question on stackoverflow (https://stackoverflow.com/questions/tagged/project-reactor).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type/bug A general bug
Projects
None yet
Development

No branches or pull requests

5 participants