Future.resultNow / exceptionNow
Paul Bjorkstrand
paul.bjorkstrand at gmail.com
Mon Nov 22 21:01:47 UTC 2021
I agree with Alex, Brian's explanation cleared up a lot of confusion for
me. I think I understand now why attaching the cause is unnecessary and I
suspect with a release, better documentation & examples will make the
mistake I made less common. Thank you for that!
While I don't have full code examples, I do have some snippets sort of
documenting a bit of my journey with StructuredExecutors + some
suggestions. See this Gist for the details, but I'll summarize suggestions
below:
https://gist.github.com/paul-bjorkstrand/43bb8335453aa1eccb982b54556d7633
Suggestions:
1. Add another built-in "handler" that is similar to ShutdownOnFailure, but
only tracks the first failure, and does not shut down the executor. This is
for cases when you want everything to try to complete, but want to report
an error that occurred.
2. Add an overload for providing a "handler" that is used as a default for
all calls of .fork() that do not provide an explicit handler. I don't have
a good use case for this one, but I could see the benefit in boilerplate
reduction.
3. Add a "handler" that does the same as #1 above, which can either be
obtained by StructuredExecutor.handler() OR build the throwIfFailed()
methods right into StructuredExecutor. Same use-cases as #1.
-Paul
On Mon, Nov 22, 2021 at 2:51 PM Alex Otenko <oleksandr.otenko at gmail.com>
wrote:
> Thank you Brian. I am in the "didn't think of a policy at all" camp, plus a
> layer of "exceptionNow provides a constructive proof of exception
> happening". The latter makes me wonder why it should throw at all - it just
> provides a witness, or none. This is similar to callbacks providing an
> exception and a result, one of them null. Having to inspect State is a bit
> of indirection, which would be fine, were it not for subtleties to watch
> out for.
>
> Similar with resultNow, just I am not suggesting to return null.
>
> Alex
>
> On Mon, 22 Nov 2021, 16:56 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