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