Future.resultNow / exceptionNow
Alex Otenko
oleksandr.otenko at gmail.com
Mon Nov 22 20:50:39 UTC 2021
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