[concurrency-interest] CompletableFuture.whenComplete survey

Timo Kinnunen timo.kinnunen at gmail.com
Sat Dec 19 11:40:44 EST 2015


Hi, 

Interesting and unexpected results, and thanks especially to everyone who provided a rationale.

I’d like to point out that the question posed in the survey isn’t the only one that needs to be considered when deciding the whenComplete method contract. A BiConsumer that rethrows the same FirstException, or throws a SecondException which has the FirstException as its cause, or throws a SecondException which suppresses the FirstException are also possibilities that must be taken into account.

That said, what I take away from the results is that there’s too much overlap between the whenComplete(BiConsumer) method and the handle(BiFunction) method. As several responses which mentioned handling indicates, it’s not clear where the responsibilities of handle end and whenComplete’s begin. The uncorrected code in the survey itself confused the two methods with each other, too! So my recommendations would then be:

- Specify the whenComplete methods in terms of the handle method.
- Rewrite the implementations of the whenComplete method to forward the call to the handle methods.
- And then deprecate whenComplete.

With this it becomes readily apparent which method to use when you want to react to the exceptional result as well and it’s clear what happens if your BiFunction throws an exception, regardless of if it was thrown intentionally or due to a bug.








-- 
Have a nice day, 
Timo

Sent from Mail for Windows 10


From: Doug Lea
Sent: Saturday, December 19, 2015 14:17
To: concurrency-interest at cs.oswego.edu
Subject: Re: [concurrency-interest] CompletableFuture.whenComplete survey


Thanks to the 71 people who answered the survey. The majority (52)
voted for the second option. To recap, here's the (fixed) question:

   CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
      if (true)
         throw new FirstException();
      else
         return "A"; });

   CompletableFuture<String> f2 = f1.whenComplete((result, exception) -> {
      if (exception != null) {
         if (true)
            throw new SecondException();
          else
            return;
      }
      else
         return; });

(where "true" stands in for some error condition).

What should the completed value of f2 be?

19: The FirstException, with the SecondException as suppressed exception
52: The SecondException, with the FirstException as suppressed exception

The vote was closer among the 29 people who filled out the optional
rationale, but still favoring the second.

One consideration differentiating votes is whether SecondException was
viewed as intentional versus the result of a programming error or
cleanup artifact. Regardless of vote outcome, throwing an exception in an
exception handler (in CompletableFutures or elsewhere) is likely to
surprise some users.

The most detailed rationale, by Tim Peierls, was
long enough that he had to place it elsewhere:
   https://gist.github.com/Tembrel/68d5670bf37824d55078

Others are below.

...

to me, it's as if the lambda is wrapped in a try-catch, with the `if (exception 
!= null)` block as the catch. As this doesn't rethrow the FirstException, f2 
should contain the thrown SecondException.

...

the most recent exception should be first, with prior ones nested

...

whenComplete shouldn't allow overwriting a normal result with a different normal 
result despite what the example code implies. Nor should it allow replacing an 
exceptional result with a different exceptional result, but going with the 
second option would be same as specifying just that, with an added wrinkle that 
the replacement exception is modified to suppress the original if it doesn't do 
that already.

...

the first exception was "handled" by the whenComplete, but not successfully

...

By using whenComplete, the user is declaring an expectation that FirstException 
may occur and are saying "I'll handle it." They haven't declared they will 
handle SecondException, so that one seems more likely (than the first) to be a 
programming error, and more important to call out.

...

per "then the returned stage exceptionally completes with this exception unless 
this stage also completed exceptionally." unless is the key word indicating f1 
exception has primacy.

...

This matches the current behavior (aside from the additional suppressed 
exception, of course). It also matches try-with-resources, the other main case 
for suppressed exceptions. (It consequently doesn't match try-finally, but we 
can match only one of the two, and try-with-resources, besides being the one 
that uses suppressed exceptions, is newer and (debatably) a replacement for most 
existing try-finally blocks.) But I admit that the try-with-resources and 
try-finally analogies aren't perfect for whenComplete(): whenComplete() has 
access to the result, but the try-with-resources close() implementation and the 
try-finally's finally do not. If whenComplete() is intended to handle other use 
cases, then those use cases may have different requirements.

...

As long as application code has the opportunity to be aware of the first 
exception and take action, it's not suppressed. Suppressing the second exception 
would mean no application code has the opportunity to even be aware of it.

...

since I specifically handle FirstException and decide to throw SecondException 
that would be the one that I expect at the end

...

f2 was supposed to handle exception from f1, but failed, so we throw 
SecondException. To not lose data, we suppress FirstException

...

First exception is likely to be the cause of a whole issue.

...

1. The contract of F2 is throw SecondException, not FirstException. 2. The 
implementor is knowingly suppressing FirstException, similar supplying the 
caused by exception when wrapping a thrown exception.

...

It should be treated the same way, when FirstException occurred in a try-block 
and SecondException occurred in finally block. I think the try-exception is the 
one which is thrown and the finally-exception is added to it as suppressed.

...

It is the least change to the current semantics; it matches the behaviour of 
try-with-resources; it lets you write a stage that releases resources without 
worrying about the exceptional path of the main code.

...
It should be isomorphic to throwing an exception in a catch block.

...

The second exception is further down the line and one can think of it as the 
transformation of the first exception, i.e., closest failure first (similar to 
how printStackTrace prints the closest method first, not the Thread's starting 
method). The third option would be to introduce some CompositeException that 
will get both exceptions suppressed, in timely order. Besides, a fluent 
java.util.concurrent.Flow library does this when lambdas crash on the onError path.

...

SecondException is the one that is last thrown.

...

For f2, SecondException is the primary exception (at the top of the ladder), and 
everything else must trail behind in the context of historicity

...

I can see reasons for both options, but in the end, I think that the caller of 
oncomplete passes in a lambda which may have a known and well defined set of 
throws, thus they are entitled to expect that f2 will only have values that can 
be thrown by the lambda

...

For consistency as an async version of a try-catch-finally block. Here "then" == 
try; "exceptionally" == catch; "when"/"handle" == finally.

...

[Option 1] mirrors try-with-resources and (arguably) is how try-finally should 
have been specified when both the try and finally blocks throw.

...

At first glance, I was viewing exception2 as the suppressed one, but then I 
wondered what happens in a chain of exceptional completions. Do suppressed 
exceptions chain? That is, what happens in a chain of exceptional completions? 
Is there a way to draw an analogy to a nesting of ARM statements? On second or 
third thought, I don't see the need for suppressed exceptions here, and worry a 
little bit about creating one automatically. I say there is not a real 
suppressed exception in this case because the first exception is "explicit" in 
the completion arguments. However, there is a chance that the earlier exception 
won't be handled by the completion code, and will be lost. And so, as a nicety 
(B) the first exception could be added as a suppressed exception of exception2.

...

Order of execution makes me feel like an exception from f1 should be the root, 
and f2 be the suppressed exception.

...

The implementor of f2 is explicitly handling the exception from f1 and throwing 
a new one. That one is the one that matters to the client of f2, at least from 
f2's perspective.

...

No time travelling to change history!

...

When ever an in-flight exception gets replaced it should become a suppressed 
exception of the new exception.

...

try-with-resources will add exceptions from close() as suppressed exceptions.






_______________________________________________
Concurrency-interest mailing list
Concurrency-interest at cs.oswego.edu
http://cs.oswego.edu/mailman/listinfo/concurrency-interest

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://cs.oswego.edu/pipermail/concurrency-interest/attachments/20151219/585c6cba/attachment-0001.html>


More information about the Concurrency-interest mailing list