Futures (was: Request for JSON-LD API review)

Kevin Gadd kevin.gadd at gmail.com
Fri Apr 19 16:02:41 PDT 2013


I'm not sure there's a perfect solution, yeah. Cancellation is definitely
not something you want every listener to be responsible for in a
multiple-listener scenario - most scenarios I've dealt with are ones where
a single task is responsible for the lifetime of a future - deciding
whether to cancel it, etc - usually the task that started it, but other
tasks may be monitoring its progress. For example, a simple 'memoization'
primitive might subscribe to a future in order to store its result when it
is completed, in order to return a cached result the next time. The
memoization primitive would never have a reason to cancel the future - that
would be up to the task that actually requested the work. So it's tricky.

.NET's standard library uses the 'cancellation token' primitive that Ron
described, and I feel that's a pretty low-risk way to encapsulate
cancellation, but it loses the benefits of having cancellation baked into
the future itself - when I cancel a task via a cancellationtoken, for any
subscribers to know about cancellation, I'll have to complete the Future
(with some sort of special TaskCancelledError instead of a result?) or drop
it on the floor and never complete it. So it creates a need for
side-channel communication in all cancellation scenarios, and it requires
all consumers to know whether or not a given Future can be cancelled. Maybe
this is unavoidable.

My particular API approach was simple, albeit not ideal: Since .NET has a
'Disposable' concept, my Future class simply became Disposable. So this
meant that in all use cases, the simplest way to get cancellation 'right'
was to use the language built-in:

var taskFuture = StartSomeTask(); // returns Future

using (taskFuture) { // when this block is left, taskFuture is disposed
  // ... do some async work using taskFuture ...
  yield return taskFuture; // wait on taskFuture
}

In practice what this meant is that when the task was suspended to wait on
taskFuture, it currently 'owned' the lifetime of that future. As a result,
if the *task itself* were cancelled, the task scheduler would dispose the
task, and because the task currently owned the lifetime of taskFuture,
disposing the task disposed taskFuture.

Cancelling an already-complete future in this manner is totally safe, so
the 'using' pattern ends up not having any downsides there - my Future
implementation is basically first-come-first-serve, where if someone stores
a result into a Future before you, they win (and you get an exception for
trying to complete it twice), and if you cancel after a result has been
stored into it the cancel is a no-op.

If you wanted to go further with the design of a task scheduler you could
automatically cancel any futures a task is waiting on, but I decided not to
do that since I didn't have an opportunity to think through all the
consequences.

Essentially in my model, the v1.0 equivalent had tri-state Futures:
Incomplete, CompletedWithResult, and CompletedWithError. Cancellation was
introduced in a later rev of the API and added a fourth 'Disposed' state.
>From a callback perspective I ended up with two callbacks, one for
'completion' (either with result or error - the premise being that if you
handle one you always want to handle the other) and another for
cancellation.

The split between functions that affect a Future and functions that consume
it is definitely an interesting one. To be honest, my API never made the
distinction - a Future is always read/write, and the state change model
generally ensures that if the Future is mishandled, an exception will be
thrown somewhere to notify you that you screwed up. But I think that
capability split is probably important, and I don't know how cancellation
fits into that model - in particular since ES6/ES7 seem very focused on
using object capability as a security model, you don't want passing a
Future across a boundary to give some third party the ability to fake the
result of a network request or something like that.


On Fri, Apr 19, 2013 at 3:50 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:

> On Fri, Apr 19, 2013 at 3:35 PM, Kevin Gadd <kevin.gadd at gmail.com> wrote:
> > My solution for cancellation has been to allow cancellation
> notifications to
> > be bidirectional - that is, when you subscribe to completion
> notifications
> > on a Future, you can also subscribe to cancellation notifications. Then
> it's
> > possible to cancel a given future without breaking any other listeners
> (as
> > long as they subscribed to cancellation notifications if they care about
> > cancellation). Has that been considered? I can see how it might be too
> > finicky for the average developer; losing out on cancellation really
> sucks
> > though.
> >
> > In particular it feels more important to have explicit cancellation built
> > into the object representing work if you can in JS, since there's no way
> to
> > lean on the garbage collector to cancel work - in environments like
> Python
> > you can make cancellation implicit by doing it when the Future
> representing
> > the work is collected, but in JS that's impossible, so having an explicit
> > way to dispose of a future is valuable, even if in many cases the
> > cancellation doesn't do anything. It's also particularly good in terms of
> > encapsulation - if there's a general cancellation mechanism that is
> > well-factored, you can just universally make a habit of cancelling
> unneeded
> > futures, and any backend implementations that support cancellation will
> > automatically get told to cancel and save cycles/bandwidth. It means that
> > you don't have to go add cancellation in 'after the fact' when the
> source of
> > a Future changes from a local buffer to a network operation, or remove
> > cancellation when you replace a network operation with a cache.
> >
> > Any kind of task scheduler like dherman's task.js can easily leverage
> this
> > to automatically cancel any task represented by a cancelled Future, and
> in
> > particular, task schedulers can propagate cancellation, by cancelling
> any of
> > the Futures a task is waiting on when the task is cancelled. This has a
> very
> > desirable property of allowing you to cancel a huge, amorphous blob of
> > pending work when it becomes unnecessary by simply cancelling the root -
> for
> > example in one application I worked on, we kicked off a task to represent
> > each avatar in a 3D scene that was responsible for loading the avatar's
> > textures, meshes, etc. If the user left the scene before the avatar was
> > fully loaded, all we had to do was cancel the task and any pending
> texture
> > loads or network requests automatically stopped. Getting that right by
> hand
> > would have been much more difficult, and we wouldn't have necessarily
> known
> > to build cancellation explicitly into that API when we started.
>
> I'm curious about what sort of API you use for this.  Right now,
> Futures are pretty easy to use, because there are only two useful
> signals that a callback has to give - successful completion or error -
> and it can do this by either returning or throwing.  It seems like
> we've run out of basic syntax to use for this kind of message-passing,
> though, and going any further would require a dedicated object.
>
> Maybe this can just be done by a second argument that is given to the
> callbacks, with a messaging object similar to the resolver object sent
> to the resolver callback?
>
> We could use this object to hold accept/reject/resolve functions too,
> in case it's convenient to be more explicit about signaling these.
> Then, though, we'd have to be careful to separate the semantics of
> "functions that affect the output future" and "functions that talk to
> the input future".
>
> ~TJ
>



-- 
-kg
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20130419/fe8405b1/attachment.html>


More information about the es-discuss mailing list