Future.resultNow / exceptionNow

Brian Goetz brian.goetz at oracle.com
Mon Nov 22 16:56:15 UTC 2021


I spent some time trying to figure out how the discussion on 
Future::resultNow could have wandered so far afield.  I think there are 
two causes:

1.  (Not-SE-facing)  Future::resultNow is intended for use with 
structured concurrency, but it is defined in Future, which is not only 
not specific to SC, but for which many people bring preconceived notions 
based on *unstructured* use.  People imagine that it is intended to be 
used (or at least, OK to use) outside the context of structured 
concurrency, where it is imaginable that someone will call it on a 
Future of unknown completion status, and immediately want to try and 
make it safer in these cases.

Our answer to this is: if you're in this situation, you're holding it 
wrong, and you should be using Future::get. Future::resultNow is 
designed for when you *already know with certainty* the status.  This 
happens routinely with structured concurrency, and may occasionally 
happen with unstructured concurrency, but the latter is not the main 
target here.  (A valid action item here is that the doc for 
Future::resultNow needs to be more emphatic about this point.)

One might be tempted to respond that even with such disclaimers, it is 
an attractive nuisance, because it is less ceremonial than Future::get, 
and therefore people will want to use it when they can, so we should 
make it more like Future::get by adding seat belts to make it more 
usable in these cases too.  While that temptation is understandable, it 
is essentially (in different words) arguing not to include it at all 
because someone might misuse it.  Which is again a valid opinion, but 
not actually what's been discussed.


2. (SE-facing) Not realizing the interplay between the result handler 
and the post-join code.  I think this has unearthed an insufficiency in 
how we present the concepts.

Implicit in each use of ES is a _policy_ for how to deal with an task 
completion (success or failure).  Examples of sensible policies include:

  - When any failure is encountered, the whole computation completes 
with failure (call this "invoke all".)

  - When any success is encountered, the whole completes successfully 
with the result of that task (call this "invoke any".)

  - Ignore failures; return a (possibly empty) list of all successes.

  - Ignore failures as in the previous, but if nothing succeeded, treat 
the whole computation as a failure.

(And policies can get crazier, like "fail if a red task succeeds before 
any blue tasks fail on days with T in the name.")

Part of using SE correctly is *knowing the completion policy* you intend 
to be using.  SE is a flexible tool, but you need a plan for how you 
intend to use it.

The implementation of a completion policy is spread across two places:

  - the handler(s) passed to SE::fork
  - the code following the call to SE::join

We have presented the handler as a lambda that takes the SE and a Future 
for the task being completed, and it might be in the simplest cases, but 
most of the time, the handler will be a stateful object that accumulates 
some state that will be interesting to the code that comes after the 
SE::join call.  The policy will contain a number of policy-specific 
outcomes, and should make it possible for the code after the join to 
find out which one happened.

There are canned handlers for the "invoke any" and "invoke all" cases, 
and they accumulate state differently, and expose different accessors 
(because they have different sets of possible outcomes).  For the 
"invoke any" (ShutdownOnSuccess), the handler terminates the computation 
early when any task completes successfully, *and* keeps track of the 
first task to complete successfully (and one of the exceptions if no 
task completed successfully).  It is expected that after the join, 
you'll ask the handler for the sole completed task with ::result (which 
throws if there are no results.)

For the "invoke all" case (ShutdownOnFailure), the handler keeps track 
of whether there was a failure, and if there was, its exception.  The 
first thing you do after joining is ask the handler if something failed; 
if nothing failed, then it is safe to assume that all tasks have 
completed successfully with a result.  Hence the example from the Javadoc:

          try (var executor = StructuredExecutor.open()) {
               var handler = new ShutdownOnFailure();

               Future<String> future1 = executor.fork(() -> query(left), 
handler);
               Future<String> future2 = executor.fork(() -> 
query(right), handler);

               executor.joinUntil(deadline);

               handler.throwIfFailed(e -> new WebApplicationException(e));

               // all tasks completed with a result
               String result = Stream.of(future1, future2)
                   .map(Future::resultNow)
                   .collect(Collectors.join(", ", "{ ", " }"));

               :
           }

I think what might be throwing some of the folks wondering why they can 
call Future::resultNow here is "how am I sure that it has completed 
successfully"; the answer is *because the handler, which you used for 
all tasks, told you so."


So, I think what is confusing people is that the following things may 
not be immediately obvious:

  - That there is a completion policy *at all*
  - That the handler is responsible for managing the state required to 
differentiate between the cases admitted by the completion policy
  - That after the join, you ask the handler what happened
  - That for each possible answer for a given handler type, there are 
known postconditions

As a matter of API design, I think part of the problem is that the fork 
method specifies the handler as a mere BiConsumer, rather than a named 
type like StructuredExecutionPolicy where we can attach some 
specification about what a policy should do. (This is of course purely 
pedagogical, not conceptual; the concepts are all there, they're 
apparently just not as obvious as we'd like them to have been.)


On 11/20/2021 11:43 AM, Alex Otenko wrote:
> Is there a strong opinion about Future.resultNow and exceptionNow throwing
> IllegalStateException? It seems there will likely be boilerplate
> try-catching, as there is no safe way to inquire in what way the Future is
> isDone.
>
> Returning Optional seems a nicer alternative.
>
> Alex


More information about the loom-dev mailing list