Future.resultNow / exceptionNow

Eric Kolotyluk eric at kolotyluk.net
Mon Nov 22 19:08:57 UTC 2021


Great analysis Brian...

IMHO, part of the problem I have with the current design is there does not
seem to be an appropriate separation of concerns, rather, concerns are
spread around to StructuredExecutor, Future, and Completion Handlers. For
example, Future has too many concerns, and resultNow() should be handled
somewhere else.

Earlier I suggested a consolation of concerns via a Session object with a
specific API for handling Structured Concurrency and related concerns; a
locus for such concerns. I imagined a StructuredConcurrencySession as the
first instance, but also imagined a more generalized Session class,
possibly an abstract class or interface. Those are just my fantasies, and
other people will imagine differently.

It is the legacy of Java to only do as much as necessary, such as Future,
then do more later, such as Callable, and eventually CompletableFuture. I
can live with that. On the other hand, I really appreciate that Future in
Scala is completable with callbacks, and is a more elegant solution than
Java. Generally, it is easier for me to reason about concurrency in Scala
than Java.

Structured Concurrency is a really powerful concept that introduces other
powerful concepts, but the current design does not feel intuitive to me.
There are too many gotchas.

Cheers, Eric

On Mon, Nov 22, 2021 at 8:57 AM Brian Goetz <brian.goetz at oracle.com> wrote:

> 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