Future cancellation
IMO, cancelation is not appropriate for promises/futures. (sticking with the name "promises", sorry for the distraction).
Promises represent a value, but have no knowledge of the work involved to get the value. The work knows about the promise, the promise doesn't know about the work. If you want to externalize additional controls to allow your code to steer the work involved in getting the promised value, that is justification for a separate class that contains a promise, likely exposing a 'then()' method linked to its internal promise. The code used to set up the resolve/reject conditions can externalize the reject method to enable cancelation for instances of that class. But the promise remains limited to a value proxy. The more a class needs to know about the work, the more specialized the class becomes, which Promise has avoided pretty well so far.
Luke
These are horribly confused -- and likely foot-gun -- designs.
First, synchronous resolution needs some justification. I don't understand why you've added it in the first design. The question of "does the Resolver know it is resolved?" is entirely independent of the visibility of that resolution to the outside world. I don't think this flag even makes sense.
In terms of blessing one or another static method for generating the cancellable tuple (as you do in your first two designs), why does it have to be in the design at this point? The current design in DOM provides a small contract to keep us from fighting over how to distinguish cancellation from other error values (the side-contract everyone would need to re-create at great expense), but purposefully avoids doing this for the sake of simplicitly. I don't get why you're trying to expand this contract in the core spec. Just subclass. Should it become so common, we can revisit this later.
Lastly, your final design is irretrievably broken in a way the first two aren't: it vends to anyone who has a handle to the Future the capability to reject it.
The DOM design has studiously avoided this class of fatal design errors so far. It's the key reason why Resolvers and Futures are separate objects. No such design will fly in this group.
Lucas Smith wrote:
IMO, cancelation is not appropriate for promises/futures. (sticking with the name "promises", sorry for the distraction).
Agreed.
2013/4/30 Alex Russell <slightlyoff at google.com>
First, synchronous resolution needs some justification. I don't understand why you've added it in the first design.
When I first read about the synchronous resolution flag I thought it made sense, for example in the context of network requests. When a network request is completed you can call resolve() with the synchronous flag because you know you're in a new tick of the event loop. It basically allows you to avoid scheduling notification in the future when you already know you're being asynchronous.
Juan
Lucas Smith wrote:
IMO, cancelation is not appropriate for promises/futures. (sticking with the name "promises", sorry for the distraction).
Agreed.
Perhaps I'm missing something, but is cancel() conceptually different from passing a Future's corresponding Resolver around so that the program can invoke reject() at will? This seems like it's a new, "irretrievably broken" way to do something you already can do.
Even if a consequence of cancellation is rejection (I'm not sure this is necessarily true), cancellation is different in that rejection comes from the source of the value, which is why it's on the resolver, while cancellation goes in the other direction - the recipient(s) of a value are telling the source that it is not needed, and the source is free to respond in any way it chooses.
I should note that in my particular case, I did not map cancellation in my API to rejection; it's a separate state with separate callbacks and pipelines in part to avoid the problem of having confusion between whether a future was rejected or cancelled. I don't think it makes much sense for cancellation to reject a future since that would cause your existing rejection callback to get invoked with a cancellation error, when the odds are that you initiated the cancellation in the first place.
I don't know what the right shape for a cancellation API is, but one should not assume that this problem is inherent to cancellation. I don't think it is. Cancellation may be hopelessly flawed as a feature in an ocap model where unprivileged consumers are sharing a single Future instance, though, because in that case I don't know how you would track the 'cancellation' state of a future across multiple unprivileged consumers, and it would probably expose a communication sidechannel just like weak references do.
Alex,
Thank you for the feedback. I’ve also added two more gists:
- Cancellation using CancellationTokenSource: gist.github.com/rbuckton/5490373
- Cancellation using CancelableFuture subclass: gist.github.com/rbuckton/5490482
Each one of these is an attempt to find out exactly what does and doesn’t work with various mechanisms for cancellation that have been discussed on these lists as well as in various libraries.
In [1] I added the synchronous flag as a way to ensure that the Future that is used for cancellation can resolve and have its resolve callbacks run in time. It’s not exactly necessary but there are some cases where not being able to cancel synchronously could bite a developer:
function someAsync(cancelFuture) { return new Future(function (resolver) { var handle = setImmediate(function () { … }); cancelFuture.done(function () { clearImmediate(handle); }); }); }
var { token, cancel } = createCanceler(); var f = someAsync(token); cancel();
Due to asynchronous resolution, even though cancel() is called synchronously it will be scheduled after the setImmediate call.
[2] was based on a side discussion around cancellation and similarities to revocable Proxies.
[3] was based on providing a simple means of cancellation, but I agree that there is a danger that a Future that is referenced by multiple consumers could be canceled by any consumer.
[4] is inspired by cooperative cancellation of Tasks in .NET. I’ve used this quite often and found it to be a powerful cancellation mechanism. It has the advantage of being very explicit about where responsibilities lie as part of cancellation and reducing pollution of the Future API, but it does add additional complexity around setup. I’m personally in favor of this approach, though not so fond of the name of types. [1] is partially based on this approach as well, except that CTS cancels synchronously and can automatically reject a Future.
[5] is a possible approach (albeit a naïve implementation) of cancellation via a subclass. In its current incarnation it suffers from the same issues a [1] and [2], but can be mitigated by having it directly call the resolver’s resolve algorithm with the synchronous flag set. It also gets lost when using Future.any, etc.
My intent currently is not to advocate any of these approaches. Rather; I’m trying to catalogue each approach as I’ve come across them specifically to gather this kind of feedback.
Best , Ron
Sent from Windows Mail
From: Alex Russell Sent: Tuesday, April 30, 2013 2:54 AM To: Ron Buckton Cc: es-discuss, public-script-coord at w3.org, Tab Atkins Jr.
These are horribly confused -- and likely foot-gun -- designs.
First, synchronous resolution needs some justification. I don't understand why you've added it in the first design. The question of "does the Resolver know it is resolved?" is entirely independent of the visibility of that resolution to the outside world. I don't think this flag even makes sense.
In terms of blessing one or another static method for generating the cancellable tuple (as you do in your first two designs), why does it have to be in the design at this point? The current design in DOM provides a small contract to keep us from fighting over how to distinguish cancellation from other error values (the side-contract everyone would need to re-create at great expense), but purposefully avoids doing this for the sake of simplicitly. I don't get why you're trying to expand this contract in the core spec. Just subclass. Should it become so common, we can revisit this later.
Lastly, your final design is irretrievably broken in a way the first two aren't: it vends to anyone who has a handle to the Future the capability to reject it.
The DOM design has studiously avoided this class of fatal design errors so far. It's the key reason why Resolvers and Futures are separate objects. No such design will fly in this group.
On Tuesday, April 30, 2013, Ron Buckton wrote: I’ve created separate gists for three different ways that I am currently investigating as a means to support the cancellation of a Future. These can be found here:
-
Cancellation using Future: https://gist.github.com/rbuckton/5486149
-
Cancellation using Future.cancelable: https://gist.github.com/rbuckton/5484591
-
Cancellation using Future#cancel: https://gist.github.com/rbuckton/5484478
Each has a list of some of the benefits and issues I’ve seen while experimenting with each approach, as well as possible changes to the various APIs or algorithms for Future to make each happen.
In general, cancellation of a Future can be beneficial in a number of cases. One example is the case where you are requesting a resource from a remote server using XHR. If the request was being made to fetch a page of data, and the user opted to move to the next page before the current page completed loading, it no longer becomes necessary to continue fetching the remote resource. In addition, it is no longer necessary to handle any additional computation or transformation logic that would have resulted from the successful completion of the fetch operation. Having the ability to cancel the request allows an application to quickly release resources that it no longer needs.
It is also useful to be able to handle the cancelation of a long running task that might be executing in a Worker. In this case, cleanup logic that is part of cancelation would request the worker to close, ending the current operation and releasing resources.
Both of the above examples are indicative of cancelling the root of an operation, but there are also circumstances where you might want to cancel a chained Future and any Future chained from it, without canceling the root. In the previous example regarding paged data, I might wish to allow the fetch operation to complete so that I could cache the data for quick retrieval, but would only want to cancel any possible UI updates that might occur in a chained Future.
I’m interested to hear what others think with respect to properly handling cancellation with Futures.
Ron
I don't know what the right shape for a cancellation API is, but one should not assume that this problem is inherent to cancellation. I don't think it is.
Take my input with a grain of salt, but I think it is.
Futures form a cascading pipeline (a series of tubes, if you will) that it promises to follow, either succeeding or failing but always continuing. Cancellation doesn't fit that model, because it involves either destructively reshaping the pipeline after it's started or trying to shove events in the wrong direction. If a value is no longer needed and you know ahead of time that it may not be, then a) keep the resolver and reject (reject() takes a value. Use it.), or b) put an if
in the success callback.
On Tue, Apr 30, 2013 at 10:26 AM, Brendan Eich <brendan at mozilla.com> wrote:
Lucas Smith wrote:
IMO, cancelation is not appropriate for promises/futures. (sticking with the name "promises", sorry for the distraction).
Agreed.
Agree as well. More conceptually, we must not conflate operations with results. "Cancel" is an action that applies to operations, not results.
Strongly agreed; however future cancellation should not be inferred to mean that a task is cancelled. Rather, it means that the value the future represents is no longer needed, which may implicitly result in the cancellation of an actual operation.
To provide a simple example (from an actual application):
I have some stack traces from a running application, and I want to map the
individual offsets in those traces to resolved symbols. This is an
expensive operation, so I do it asynchronously.
So, I have a function that maps a given offset to a resolved symbol, with a
signature like Future<Symbol> getSymbolForOffset (uint offset)
. This
function returns a future that will be completed when the symbol is
available.
Now, each one of these calls does not necessarily map to a single symbol
resolution operation - symbol resolution is expensive, so I may batch
requests up into larger groups to reduce round-trips and improve
performance.
Given this, I have a many-to-one mapping - many futures mapping to a single
operation. Simply exposing the ability to cancel that operation doesn't
make any sense; the underlying operation is an implementation detail. And,
as you mentioned, cancelling one of those futures should not cancel the
operation; the ability to cancel that operation directly should not be
exposed to an owner of just one of those futures.
Regardless, by being able to 'dispose' or mark as unneeded the individual
futures, you can ensure that if all the futures an operation is about to
fulfill are not needed, the operation can automatically be cancelled.
I see it as difficult to satisfy these scenarios without some sort of approach to future disposal/cancellation, and I see this as separate from questions of how one goes about cancelling tasks, even if task cancellation is sometimes driven by future disposal/cancellation.
-kg
On Tue, Apr 30, 2013 at 12:23 PM, Kevin Smith <zenparsing at gmail.com> wrote:
On Tue, Apr 30, 2013 at 10:26 AM, Brendan Eich <brendan at mozilla.com>wrote:
Lucas Smith wrote:
IMO, cancelation is not appropriate for promises/futures. (sticking with the name "promises", sorry for the distraction).
Agreed.
Agree as well. More conceptually, we must not conflate operations with results. "Cancel" is an action that applies to operations, not results.
{ Kevin }
es-discuss mailing list es-discuss at mozilla.org, mail.mozilla.org/listinfo/es-discuss
-------------- next part -------------- An HTML attachment was scrubbed... URL: esdiscuss/attachments/20130430/17f86859/attachment
On Mon, Apr 29, 2013 at 6:57 PM, Ron Buckton <rbuckton at chronicles.org> wrote:
I’ve created separate gists for three different ways that I am currently investigating as a means to support the cancellation of a Future. These can be found here:
Cancellation using Future: https://gist.github.com/rbuckton/5486149
Cancellation using Future.cancelable:
gist.github.com/rbuckton/5484591
Cancellation using Future#cancel:
gist.github.com/rbuckton/5484478
Each has a list of some of the benefits and issues I’ve seen while experimenting with each approach, as well as possible changes to the various APIs or algorithms for Future to make each happen.
In general, cancellation of a Future can be beneficial in a number of cases. One example is the case where you are requesting a resource from a remote server using XHR. If the request was being made to fetch a page of data, and the user opted to move to the next page before the current page completed loading, it no longer becomes necessary to continue fetching the remote resource. In addition, it is no longer necessary to handle any additional computation or transformation logic that would have resulted from the successful completion of the fetch operation. Having the ability to cancel the request allows an application to quickly release resources that it no longer needs.
It is also useful to be able to handle the cancelation of a long running task that might be executing in a Worker. In this case, cleanup logic that is part of cancelation would request the worker to close, ending the current operation and releasing resources.
Both of the above examples are indicative of cancelling the root of an operation, but there are also circumstances where you might want to cancel a chained Future and any Future chained from it, without canceling the root. In the previous example regarding paged data, I might wish to allow the fetch operation to complete so that I could cache the data for quick retrieval, but would only want to cancel any possible UI updates that might occur in a chained Future.
I’m interested to hear what others think with respect to properly handling cancellation with Futures.
I do not think that we should add cancellation on the base Future interface. I.e. we shouldn't make all Futures cancellable.
Cancelability should only be possible when the implementation of the Future would actually stop doing work if the Future is cancelled. I.e. cancelling a Future shouldn't simply prevent the result callbacks from being called, but it should prevent whatever work is needed to calculate the result from happening.
However it would be very complex and expensive if we had to make all APIs that want to use Futures also support being cancelled.
The solution is to create a subclass of Future which allows the back-end work to be cancelled. I.e. a CancelableFuture, or AbortableFuture. This subclass would have a .cancel() or .abort() method on it. The FutureResolver created when the CancelableFuture is created would have a callback which is called when .cancel()/.abort() is called.
This would be useful if we create an Future-based API for doing network requests or file reading.
In other words, the should be the choice of the implementor of a given API to determine if it wants to return a Future which can be cancelled, or one that can't. Obviously this needs to be documented for that API, just like you document that the API returns a Future at all.
/ Jonas
Jonas,
As I mentioned to Alex Russel, I'm also interested in the benefits of using a subclass. I have two additional gists around cancellation, one that uses an external token for cancelation [1] (similar to .NET/C#), and another that uses a subclass [2] (though I still need to spend some additional time on the subclass sample).
[1] gist.github.com/rbuckton/5490373 [2] gist.github.com/rbuckton/5490482
I agree with the sentiment that Future#cancel is a bad idea.
Ron
Jonas Sicking wrote:
I do not think that we should add cancellation on the base Future interface. I.e. we shouldn't make all Futures cancellable.
Cancelability should only be possible when the implementation of the Future would actually stop doing work if the Future is cancelled. I.e. cancelling a Future shouldn't simply prevent the result callbacks from being called, but it should prevent whatever work is needed to calculate the result from happening.
However it would be very complex and expensive if we had to make all APIs that want to use Futures also support being cancelled.
You seem to be arguing based on the word "cancel". The semantic would rather be "Please note that I am no longer interested in this value", which would make sense even if the "work" is not actually "stopped", and, accordingly, it would not be complex and expensive to support.
In other words, I think most people would agree that a "forced stop" option for everything is complex and expensive and should not be re- quired, but that does not necessarily invalidate the ideas that moti- vate a "cancel" option.
The solution is to create a subclass of Future which allows the back-end work to be cancelled. I.e. a CancelableFuture, or AbortableFuture. This subclass would have a .cancel() or .abort() method on it. The FutureResolver created when the CancelableFuture is created would have a callback which is called when .cancel()/.abort() is called.
"Future" "subclasses" seem rather dubious to me.
I think the subclass is the way to go.
However there are some tricky issues that need to be resolved:
What happens if the Future is cancelled after it has been resolved? To stick with the Future API, once you've started delivering results, you shouldn't change what result is delivered.
What happens if cancel() is called multiple times?
Then there's of course the issue of what we should do with APIs that combine several Futures into a single one. Like Future.every() etc.
Similarly, there's also the issue of what to do with chaining.
I'm tempted to say that if you create combined or dependent Futures, you still only have the ability to cancel them through the original CancelableFuture.
Like others have pointed out here, we need to keep "operations" separate from "delivering results". Combined and dependent Futures are combining the delivering of results, but they don't combine the operations.
/ Jonas
On Tue, Apr 30, 2013 at 5:56 PM, Bjoern Hoehrmann <derhoermi at gmx.net> wrote:
- Jonas Sicking wrote:
I do not think that we should add cancellation on the base Future interface. I.e. we shouldn't make all Futures cancellable.
Cancelability should only be possible when the implementation of the Future would actually stop doing work if the Future is cancelled. I.e. cancelling a Future shouldn't simply prevent the result callbacks from being called, but it should prevent whatever work is needed to calculate the result from happening.
However it would be very complex and expensive if we had to make all APIs that want to use Futures also support being cancelled.
You seem to be arguing based on the word "cancel". The semantic would rather be "Please note that I am no longer interested in this value", which would make sense even if the "work" is not actually "stopped", and, accordingly, it would not be complex and expensive to support.
In other words, I think most people would agree that a "forced stop" option for everything is complex and expensive and should not be re- quired, but that does not necessarily invalidate the ideas that moti- vate a "cancel" option.
"I am no longer interested in receiving this value" is definitely a separation operation than "I want the operation to be stopped".
If you're no longer interested in receiving a particular value, that may or may not mean that others are interested in that value. Futures support multiple callers registering to be told about a particular result. And, as importantly, they support people registering to receive a result at any point in time.
If we want to support the semantics of "I am no longer interested in receiving this value" then I propose that we add a way to "unregister" an individual .then() callback. But I'm not at all sure that this is a use case that is important enough to implement. Callers can always ignore the value once it comes in.
/ Jonas
On Tue, Apr 30, 2013 at 7:37 PM, Jonas Sicking <jonas at sicking.cc> wrote:
Then there's of course the issue of what we should do with APIs that combine several Futures into a single one. Like Future.every() etc.
Similarly, there's also the issue of what to do with chaining.
I'm tempted to say that if you create combined or dependent Futures, you still only have the ability to cancel them through the original CancelableFuture.
Like others have pointed out here, we need to keep "operations" separate from "delivering results". Combined and dependent Futures are combining the delivering of results, but they don't combine the operations.
There are actually some very big similarities here with ProgressFuture. There too we are facing the question of what to do if multiple Futures, some of which are ProgressFutures, are combined using Future.every, or what to do if a Future is chained after a ProgressFuture.
It isn't actually surprising that the same issues arise. ProgressFuture basically delivers progress about an "operation" rather than a "result".
My gut instinct is still the same. Combining results using dependent or combined Futures is great. However trying to combine operations seems like a lot of complexity and easily a source of confusion.
/ Jonas
From: es-discuss-bounces at mozilla.org [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Jonas Sicking
It isn't actually surprising that the same issues arise. ProgressFuture basically delivers progress about an "operation" rather than a "result".
I agree. I think both progress and cancellation (of underlying operations) are attractive nuisances. They seem like they fit in well with the model, and would be useful for certain use cases. But they are actually very different, and not tied to the underling promise semantics at all—which are of a first class value representing a promised "result," as you put it, not representing an ongoing "operation."
I lean toward not specifying or including them at all. Although you can try to stuff them naively into the promise semantics, you end up needing to complicate the conceptual model in order to make them behave as you wish. As you point out, this is clearly visible with the various combinators. But it's also visible in basic semantic questions that arise: e.g. downward progress propagation/transformation, or the role of throwing inside a progress handler, or upward cancellation progagation/reaction, or downward cancellation forking, or the role of rejections in cancellation. You soon realize that you're trying to smush in semantics where they don't belong.
In other words, separate abstractions for cancellation or progress, unrelated to promises, seem best.
To be honest, I haven't yet seen an explanation of why 'I no longer have any need for the contents of this future' is not tied to the underlying semantics of a future. Where else would it go? I agree with you that complicating a primitive in order to support a small subset of use cases is undesirable, and it does seem like perhaps end-user subclassing of futures/promises addresses all these use cases. But it is troubling to me to consistently see real use cases dismissed as somehow unworthy of consideration because they do not meet some unspecified semantic bar.
And to restate some points that have cropped up in the thread multiple times so far, since they are relevant to your arguments:
Future cancellation is not the same as task cancellation; it is different. I would agree with any suggestion that using the names 'cancel' or 'abort' probably leads people to assume that they are the same. I prefer the term 'disposal' but that is perhaps overly .NETty and implies things that do not apply to JS.
Layering disposal onto a promise from the outside is fine, but requires that all consumers of an API have prior knowledge of the way the API is implemented; that is, the API author has to expose disposal beforehand (even if it is not used) and the consumer has to know that it is needed. This exposes whether or not disposal is actually used by an implementation when it should be an implementation detail. Much like how - to pick an arbitrary example - finally blocks in a generator should run if you enumerate the generator with for-of, it is desirable to have a universal way to indicate that a future's value is unneeded by a single consumer such that any given API implementer can extend their implementation seamlessly to abort operations that are no longer needed. This allows significant changes to the backend of an operation (caching, batching, etc) without changing the public API. (Note that you can probably address this with a Future subclass instead of baking disposal into Future itself, or by exposing some sort of public function that you can call on a future to indicate that you are done with it and making that function a no-op for non-disposable futures. I don't have a strong opinion).
I'm fine with cancellation and progress being nuked from futures for conceptual purity; simpler is probably better. But if you're going to kill them, you should do it based on sound reasoning, otherwise they're just going to keep cropping up again and again because people want them and will probably not accept a justification that seems based on a misunderstanding of their arguments.
Given the choice between explicitly not specifying disposal, and specifying an 'encouraged way to do it' (i.e. via subclassing), I would also lean towards the latter. That is, if you're going to say 'we don't need to spec disposal', by demonstrating an incredibly easy way to do it via subclassing, you address any arguments that it must be built into the language or into DOM futures, and you demonstrate a pattern that is expected to work based on the current spec.
From: Domenic Denicola [mailto:domenic at domenicdenicola.com]
From: es-discuss-bounces at mozilla.org [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Jonas Sicking
It isn't actually surprising that the same issues arise. ProgressFuture basically delivers progress about an "operation" rather than a "result".
I agree. I think both progress and cancellation (of underlying operations) are attractive nuisances. They seem like they fit in well with the model, and would be useful for certain use cases. But they are actually very different, and not tied to the underling promise semantics at all?which are of a first class value representing a promised "result," as you put it, not representing an ongoing "operation."
I lean toward not specifying or including them at all. Although you can try to stuff them naively into the promise semantics, you end up needing to complicate the conceptual model in order to make them behave as you wish. As you point out, this is clearly visible with the various combinators. But it's also visible in basic semantic questions that arise: e.g. downward progress propagation/transformation, or the role of throwing inside a progress handler, or upward cancellation progagation/reaction, or downward cancellation forking, or the role of rejections in cancellation. You soon realize that you're trying to smush in semantics where they don't belong.
In other words, separate abstractions for cancellation or progress, unrelated to promises, seem best.
This is roughly the case with .NET/C# cancellation. In .NET you use an external object to represent cancellation. If a function returns a Future (or "Task" in .NET), it's the function author's decision as to whether they want to accept a cancellation token. This allows the author control over whether they want to support cancellation. It's up to the caller to choose to construct a cancellation token and feed it to the function call. This allows for separation of responsibilities between the Future (which is only responsible for delivering asynchronous completion) and Cancellation (which can provide control over the operation). In .NET though, Tasks do participate in cancellation. They have a unique internal state that represents a cancelled task and a level of control over how to handle chained continuations (via the TaskContinuationOptions enum).
The same can be said for progress notifications in .NET Tasks, as they require a synchronization context to post progress notifications back to the caller who may be in a different thread. This does illustrate your point though, regarding how Futures, cancellation, and progress are related but distinct.
I think the .NET disposal analogy is a good one. I don?t know how other runtimes handle it, but in .NET you would dispose of a resource (perhaps representing an in-progress asynchronous operation) by having the handle for that resource implement IDisposable, i.e. have a dispose() method. So I think if you wanted to do something similar with promises, you?d return a disposable handle alongside your promise (i.e. return { promise, diposable }
or simply { promise, dispose }
). You can also layer that on top of the promise (i.e. promise.dispose = () => { .. }; return promise;
).
I think that makes lots of sense. Would the destructuring assignment, i.e. {promise, dispose} = ... work for functions that return a single result (just a promise) instead of a result or a disposer?
My main concern about intentionally treating disposal as a non-issue is that it ensures that it will take a long time for disposal to ever make its way into common libraries and applications; that is, for disposal to work, both library authors and consumers need to adopt it. Any pattern that at least makes it possible to incrementally add disposal in over time, from the library side and the application side, is optimal - whether you do it by checking for 'promise.dispose' as a callable, or checking for a 'dispose' second result from functions, or checking to see if a given promise is branded with some special symbol. This is also why the cancellation token method is something it worries me to see proposed - it's a great approach from an engineering perspective, but arguably a large reason why it worked in .NET is because Microsoft had the ability to make it the de-facto approach to cancellation and force people to adopt it.
Another option that came to mind is that you could use WeakMap to store the disposal state of a given future. Any consumer that cares about disposal can set the flag in the shared weakmap (perhaps through some third party library that encapsulates the disposal state map) and the producers that generate futures can check the WeakMap before performing an operation for a given set of futures. I don't know enough to comment on whether or not this would be a scalable and optimal solution, but I think it does directly address the problem of adding cancellation to futures when in many use cases they will not be of any value. For ocap security you would probably need to carefully guard access to that weakmap by exposing reads/writes through privileged functions or something.
On Tue, Apr 30, 2013 at 7:26 AM, Brendan Eich <brendan at mozilla.com> wrote:
Lucas Smith wrote
IMO, cancelation is not appropriate for promises/futures. (sticking with the name "promises", sorry for the distraction).
Agreed.
Glad to see everyone agreeing on something about promises for a change. Unfortunately, I think you're all wrong. ^_^
Every future is "cancellable" already. If you hand out your resolver, anyone with it can preempt you and (prematurely?) fulfill the promise, before your intended code-path finishes. There are only two small differences between normal promises and ones that are explicitly "cancellable":
-
Some promises are holding onto resources (locks, network connections, cpu time) which will be disposed of when they're finished. If you want to allow someone else to pre-empt you, you need to be able to release these resources when that happens, so you're not spinning your wheels doing work only to make a useless "resolver.accept(dead-value)" that just gets ignored because the promise is already fulfilled. So, the constructor for the promise needs some way to register some teardown code, called when the promise is fulfilled.
-
The default case for cancellable promises is that they want their promise consumers to be able to cancel them, as opposed to normal promises that usually want to maintain complete control over their fulfillment. So, you probably want to return something with resolving powers by default, in addition to the promise itself.
I think this is more than acceptable for a subclass, and I think it's quite simple to do:
-
the constructor should take a second callback, which is called with no arguments when the promise is fulfilled by any mechanism, and which is intended for teardown code. It has no effect on the promise's state.
-
The return value of the constructor should be a {promise, resolver} pair, rather than just the promise itself. This maintains the separation of capabilities, but lets the consumer kill the promise if they don't need it anymore.
This design adds minimal new surface area, solves the necessary problems, and lets consumers either accept or reject the promise when they cancel it, so they can provide a "default" value to other consumers transparently.
On Tue, Apr 30, 2013 at 10:01 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
I think the .NET disposal analogy is a good one. I don?t know how other runtimes handle it, but in .NET you would dispose of a resource (perhaps representing an in-progress asynchronous operation) by having the handle for that resource implement IDisposable, i.e. have a dispose() method. So I think if you wanted to do something similar with promises, you?d return a disposable handle alongside your promise (i.e.
return { promise, diposable }
or simply{ promise, dispose }
). You can also layer that on top of the promise (i.e.promise.dispose = () => { .. }; return promise;
).
I don't see how the "layer on top" solution is different from subclassing? In both cases you get an object which implements the Future interface and additionally has a .cancel()/.dispose() method on it. The only difference appears to be the name?
I don't really see the benefit of returning a { promise, dispose } tuple as result comparsed to the layering/subclassing solution.
With the tuple approach, you get two objects one which represents the operation and one which represents the result. The result object can be composed with other promises or you simply register to wait for the result. So something like:
{ promise, dispose } = doSomeOperation();
handleResult(promise);
cancelIfUserClicksAbort(dispose);
or
{ promise, dispose } = doSomeOperation();
cancelIfUserClicksAbort(dispose);
return promise;
or
{ promise, dispose } = doSomeOperation();
cancelIfUserClicksAbort(dispose);
promise.then(val => displayResult(val));
with the layering/subclassing approach you do essentially exactly the same thing, except you use a single object rather than two:
cancelableFuture = doSomeOperation();
handleResult(cancelableFuture);
cancelIfUserClicksAbort(cancelableFuture);
or
cancelableFuture = doSomeOperation();
cancelIfUserClicksAbort(cancelableFuture);
return cancelableFuture;
or
cancelableFuture = doSomeOperation();
cancelIfUserClicksAbort(cancelableFuture);
cancelableFuture.then(val => displayResult(val));
However with this approach you get an API which automatically simply works as an API returning a Future in case you don't need to abort the operation or display its progress:
handleResult(doSomeOperation());
or
return doSomeOperation();
or
doSomeOperation().then(val => displayResult(val));
I.e. if you don't care about the operation part, the API simply works as any other API which returns a promise. This seems like a very nice thing. The only "cost" of this API is that it doesn't compose when you compose the future, but neither does the dispose object in the tuple approach.
Thanks for engaging and not being put off by my previous message. I understand it could have come across as rather abrasive. Apologies for that. More inline.
On Tuesday, April 30, 2013, Ron Buckton wrote:
Alex,****
Thank you for the feedback. I’ve also added two more gists:****
- Cancellation using CancellationTokenSource: * gist.github.com/rbuckton/5490373*gist.github.com/rbuckton/5490373
This is interesting. Is there a reason to allow configuration of the cancelation token? I had thought the value in having a standard mechanism (and value for "this future was canceled") would be to avoid per-API contracts of the sort that might be created here.
Also, if it's really necessary but turns out to be rare, we might imagine Future/Resolver subclass pairs that are pre-configured with different cancel values instead of configuration. But I don't have experience with how common cancelation through chains of Futures with different cancelation roots is.
- Cancellation using CancelableFuture subclass: * gist.github.com/rbuckton/5490482*gist.github.com/rbuckton/5490482
This design is roughly what I'd sketched out for Anne van Kesteren as what DOM will need to do for XHR as it already vends the cancelation capability there.
Our designs for this diverged in that I hadn't overridden then/catch to enable chaining, instead relying on the "cancel" error value in rejection.
Each one of these is an attempt to find out exactly what does and doesn’t work with various mechanisms for cancellation that have been discussed on these lists as well as in various libraries. ****
In [1] I added the synchronous flag as a way to ensure that the Future that is used for cancellation can resolve and have its resolve callbacks run in time. It’s not exactly necessary but there are some cases where not being able to cancel synchronously could bite a developer:****
function someAsync(cancelFuture) {****
return new Future(function (resolver) {****
var handle = setImmediate(function () { … });**** cancelFuture.done(function () { clearImmediate(handle); });****
});****
}****
var { token, cancel } = createCanceler();****
var f = someAsync(token);****
cancel();****
Due to asynchronous resolution, even though cancel() is called synchronously it will be scheduled after the setImmediate call.****
**
This honestly feels like a separate side contract about what it means to cancel some underlying operation, not what it means to get the resolution value...and it feels like the sort of thing that cancelable futures need to navigate with to the semantics of the underlying operation that they're representing. Put another way, it's about what resolvers say to resolvers and when. ISTM that any future subclass should be able to schedule synchronous resolution for cancel if it wants to. I don't think that breaks any invariants of the design. The question now is where that belongs. Punning too strongly on the superclass resolution semantics feels wrong, but so does carving out brand new API space for synchronous resolution until we have at least one more use-case for it. For now, we could simply say that CancelableFuture cancels synchronously and leave that up to impls to accomodate. Feels dirty, but preserves the ability to explain it later with a synchronous flag should we find it more broadly necessary.
**
[2] was based on a side discussion around cancellation and similarities to revocable Proxies.****
[3] was based on providing a simple means of cancellation, but I agree that there is a danger that a Future that is referenced by multiple consumers could be canceled by any consumer.****
[4] is inspired by cooperative cancellation of Tasks in .NET. I’ve used this quite often and found it to be a powerful cancellation mechanism. It has the advantage of being very explicit about where responsibilities lie as part of cancellation and reducing pollution of the Future API, but it does add additional complexity around setup. I’m personally in favor of this approach, though not so fond of the name of types. [1] is partially based on this approach as well, except that CTS cancels synchronously and can automatically reject a Future.****
[5] is a possible approach (albeit a naïve implementation) of cancellation via a subclass. In its current incarnation it suffers from the same issues a [1] and [2], but can be mitigated by having it directly call the resolver’s resolve algorithm with the synchronous flag set. It also gets lost when using Future.any, etc. ****
My intent currently is not to advocate any of these approaches. Rather; I’m trying to catalogue each approach as I’ve come across them specifically to gather this kind of feedback.
Much appreciated. I've fretted for some time that I've under-researched the cancellation aspects of the design (whereas I don't have any such qualms about the rest of it).
On Wednesday, May 1, 2013, Jonas Sicking wrote:
On Mon, Apr 29, 2013 at 6:57 PM, Ron Buckton <rbuckton at chronicles.org<javascript:;>> wrote:
I’ve created separate gists for three different ways that I am currently investigating as a means to support the cancellation of a Future. These can be found here:
Cancellation using Future:
gist.github.com/rbuckton/5486149
Cancellation using Future.cancelable:
gist.github.com/rbuckton/5484591
Cancellation using Future#cancel:
gist.github.com/rbuckton/5484478
Each has a list of some of the benefits and issues I’ve seen while experimenting with each approach, as well as possible changes to the various APIs or algorithms for Future to make each happen.
In general, cancellation of a Future can be beneficial in a number of cases. One example is the case where you are requesting a resource from a remote server using XHR. If the request was being made to fetch a page of data, and the user opted to move to the next page before the current page completed loading, it no longer becomes necessary to continue fetching the remote resource. In addition, it is no longer necessary to handle any additional computation or transformation logic that would have resulted from the successful completion of the fetch operation. Having the ability to cancel the request allows an application to quickly release resources that it no longer needs.
It is also useful to be able to handle the cancelation of a long running task that might be executing in a Worker. In this case, cleanup logic that is part of cancelation would request the worker to close, ending the current operation and releasing resources.
Both of the above examples are indicative of cancelling the root of an operation, but there are also circumstances where you might want to cancel a chained Future and any Future chained from it, without canceling the root. In the previous example regarding paged data, I might wish to allow the fetch operation to complete so that I could cache the data for quick retrieval, but would only want to cancel any possible UI updates that might occur in a chained Future.
I’m interested to hear what others think with respect to properly handling cancellation with Futures.
I do not think that we should add cancellation on the base Future interface. I.e. we shouldn't make all Futures cancellable.
Cancelability should only be possible when the implementation of the Future would actually stop doing work if the Future is cancelled. I.e. cancelling a Future shouldn't simply prevent the result callbacks from being called, but it should prevent whatever work is needed to calculate the result from happening.
However it would be very complex and expensive if we had to make all APIs that want to use Futures also support being cancelled.
The solution is to create a subclass of Future which allows the back-end work to be cancelled. I.e. a CancelableFuture, or AbortableFuture. This subclass would have a .cancel() or .abort() method on it. The FutureResolver created when the CancelableFuture is created would have a callback which is called when .cancel()/.abort() is called.
This is what I've sketched in various places, including for Anne WRT XHR. I suppose (without any cause) that folks would pick up on the idea that the minimal Future superclass was being explicitly designed to be subclassable to address issues like this and progress notification. Perhaps we need to call it out more explicitly in the spec?
On Wed, May 1, 2013 at 10:45 AM, Alex Russell <slightlyoff at google.com> wrote:
This is what I've sketched in various places, including for Anne WRT XHR. I suppose (without any cause) that folks would pick up on the idea that the minimal Future superclass was being explicitly designed to be subclassable to address issues like this and progress notification. Perhaps we need to call it out more explicitly in the spec?
The specification does need to address that better, in particular what then() and catch() might return for subclassed futures. We found use cases both for returning a new instance of the subclassed future itself (ProgressFuture) and simply returning a new "base" future (for the crypto APIs).
I think we want to define some of the common ones directly in the specification. That will a) help people designing their own and b) encourage some level of consistency.
[General comments here, specifics inline.]
My experiences with promises is as an E programmer. When they are used pervasively in an application the graph can get as messy as the reference graph of an object oriented program. For languages with objects and references, garbage collection became the generally accepted way of cleaning up the mess (c.f. C++). For promises, breaking the promise allows the graph to be cleaned up.
On 4/30/13 at 11:04 PM, jackalmage at gmail.com (Tab Atkins Jr.) wrote:
Every future is "cancellable" already. If you hand out your resolver, anyone with it can preempt you and (prematurely?) fulfill the promise, before your intended code-path finishes. There are only two small differences between normal promises and ones that are explicitly "cancellable":
This comment seems to be the essence of the issue. You can hold the resolver at the edge of a abortable computation. When you decide to abort the computation, perhaps because one or more consumers have indicated they are no longer interested in the results, you can use the resolver to resolve the promise as "broken". That broken promise will filter up through all computations which depend on it's value, allowing them to proceed knowing that the value will not be produced.
- Some promises are holding onto resources (locks, network connections, cpu time) which will be disposed of when they're finished.
I think this statement is wrong. Promises don't hold resources. They are a placeholder for a value to be provided later. Perhaps the computation which may provide the value at some future time holds a resource, or the computation which will consume the value when it is resolved holds a resource (generally a poor programming practice), but the promise itself doesn't hold resources.
Cheers - Bill
Bill Frantz | Concurrency is hard. 12 out | Periwinkle (408)356-8506 | 10 programmers get it wrong. | 16345 Englewood Ave www.pwpconsult.com | - Jeff Frantz | Los Gatos, CA 95032
The specification does need to address that better, in particular what then() and catch() might return for subclassed futures. We found use cases both for returning a new instance of the subclassed future itself (ProgressFuture) and simply returning a new "base" future (for the crypto APIs).
I think this difficulty points to a deeper issue with attempting to make a promise something other than a placeholder for a value. Namely: it's no longer obvious how the information or abilities stored in the promise itself should propagate through the graph. The featureless-ness of promises is one of their most important features.
2013/5/1 Anne van Kesteren <annevk at annevk.nl>
On Wed, May 1, 2013 at 10:45 AM, Alex Russell <slightlyoff at google.com> wrote:
This is what I've sketched in various places, including for Anne WRT XHR. I suppose (without any cause) that folks would pick up on the idea that the minimal Future superclass was being explicitly designed to be subclassable to address issues like this and progress notification. Perhaps we need to call it out more explicitly in the spec?
The specification does need to address that better, in particular what then() and catch() might return for subclassed futures. We found use cases both for returning a new instance of the subclassed future itself (ProgressFuture) and simply returning a new "base" future (for the crypto APIs).
For YUI we tried something like:
then(callback, errback) { return new this.constructor(function (resolver) { // ... }); }
While it is true that there are use cases for both, there are use cases that get broken when returning a instance of the subclass. For example, a LazyPromise which runs the initialization function only when then() is called, breaks down with this approach. It seems to me that if some use cases break and for others the same effect can be achieved using a different approach, then the safest path should be taken. That seems to be only returning "base" promises from then().
2013/5/1 Kevin Smith <zenparsing at gmail.com>
I think this difficulty points to a deeper issue with attempting to make a
promise something other than a placeholder for a value.
The fact is that in a way promises are already a representation for a value and for an operation. What is a promise for undefined? Let's say I have a database object with a close() method that works asyncronously. If close() returns a promise, which value does it represent?
As for cancellation, I worry about the ergonomics of returning { promise, cancel } pairs. Like Alex mentioned, a subclass makes a lot more sense for XMLHttpRequest. And if it works for XHR why shouldn't it work for other promises?
Juan
On Wed, May 1, 2013 at 5:50 AM, Bill Frantz <frantz at pwpconsult.com> wrote:
On 4/30/13 at 11:04 PM, jackalmage at gmail.com (Tab Atkins Jr.) wrote:
Every future is "cancellable" already. If you hand out your resolver, anyone with it can preempt you and (prematurely?) fulfill the promise, before your intended code-path finishes. There are only two small differences between normal promises and ones that are explicitly "cancellable":
This comment seems to be the essence of the issue. You can hold the resolver at the edge of a abortable computation. When you decide to abort the computation, perhaps because one or more consumers have indicated they are no longer interested in the results, you can use the resolver to resolve the promise as "broken". That broken promise will filter up through all computations which depend on it's value, allowing them to proceed knowing that the value will not be produced.
I think it's reasonably valuable to allow cancelers to choose whether to cancel the computation with an error, or to cancel it with another value. For example, you could race an XHR promise and a timeout promise, and if the timeout finishes first, it cancels the XHR with a default value.
(Hm, on the other hand, this use-case is already taken care of by Future.any(), especially if we spec that Future.any() auto-cancels any cancellable futures passed to it if they don't resolve in time.)
- Some promises are holding onto resources (locks, network connections, cpu time) which will be disposed of when they're finished.
I think this statement is wrong. Promises don't hold resources. They are a placeholder for a value to be provided later. Perhaps the computation which may provide the value at some future time holds a resource, or the computation which will consume the value when it is resolved holds a resource (generally a poor programming practice), but the promise itself doesn't hold resources.
Semantics. ^_^ The promise can represent a computation, which can hold some releasable resources.
Jonas Sicking wrote:
Then there's of course the issue of what we should do with APIs that combine several Futures into a single one. Like Future.every() etc.
Similarly, there's also the issue of what to do with chaining.
I'm tempted to say that if you create combined or dependent Futures, you still only have the ability to cancel them through the original CancelableFuture.
And the "progress" of multiple "Futures" can only be observed through the individual "ProgressFuture" objects? I would expect the opposite. Similarily, I would expect to be able to mix "ProgressFuture" objects with other "Future" objects, and still be able to observe "progress" of the combination. And if I can do that, I would also expect that I can turn a single "Future" into a "ProgressFuture" in this sense, but then the whole "subclassing" idea kinda breaks down, why bother with that. And "cancelation" does not seem quite so different from "pro- gress" in this sense.
On Wed, May 1, 2013 at 12:16 AM, Jonas Sicking <jonas at sicking.cc> wrote:
However with this approach you get an API which automatically simply works as an API returning a Future in case you don't need to abort the operation or display its progress:
handleResult(doSomeOperation()); or return doSomeOperation(); or doSomeOperation().then(val => displayResult(val));
I.e. if you don't care about the operation part, the API simply works as any other API which returns a promise. This seems like a very nice thing. The only "cost" of this API is that it doesn't compose when you compose the future, but neither does the dispose object in the tuple approach.
The other cost is an inherent capability leak - unless you purposely strip it of its cancelability, passing it around to anything else gives the "anything else" the ability to cancel your promise as well. The tuple approach doesn't have this issue - the promise and the canceler/resolver are inherently separated, and you have to purposely hand the canceler/resolver off to other code for it to have any powers.
On May 1, 2013 8:21 AM, "Tab Atkins Jr." <jackalmage at gmail.com> wrote:
On Wed, May 1, 2013 at 12:16 AM, Jonas Sicking <jonas at sicking.cc> wrote:
However with this approach you get an API which automatically simply works as an API returning a Future in case you don't need to abort the operation or display its progress:
handleResult(doSomeOperation()); or return doSomeOperation(); or doSomeOperation().then(val => displayResult(val));
I.e. if you don't care about the operation part, the API simply works as any other API which returns a promise. This seems like a very nice thing. The only "cost" of this API is that it doesn't compose when you compose the future, but neither does the dispose object in the tuple approach.
The other cost is an inherent capability leak - unless you purposely strip it of its cancelability, passing it around to anything else gives the "anything else" the ability to cancel your promise as well. The tuple approach doesn't have this issue - the promise and the canceler/resolver are inherently separated, and you have to purposely hand the canceler/resolver off to other code for it to have any powers.
This is a problem that the current XHR API is suffering from too. Has that been a problem in reality?
With XHR you can even affect the values that other consumers are receiving by setting .response type. Again, does anyone have examples of when this has been a problem?
I feel like attempting to solve this problem will mean that we need to split a lot of interfaces into lists of objects, each representing a capability. This seems like it will very quickly lead to an unmanageable mess.
Wrapping a CancelableFuture in a normal Future seems like an acceptable solution which only adds complexity in the much more rare case that someone cares about capabilities.
/ Jonas
On 5/1/13 at 7:54 AM, jackalmage at gmail.com (Tab Atkins Jr.) wrote:
On Wed, May 1, 2013 at 5:50 AM, Bill Frantz <frantz at pwpconsult.com> wrote:
On 4/30/13 at 11:04 PM, jackalmage at gmail.com (Tab Atkins Jr.) wrote: ...
- Some promises are holding onto resources (locks, network connections, cpu time) which will be disposed of when they're finished.
I think this statement is wrong. Promises don't hold resources. They are a placeholder for a value to be provided later. Perhaps the computation which may provide the value at some future time holds a resource, or the computation which will consume the value when it is resolved holds a resource (generally a poor programming practice), but the promise itself doesn't hold resources.
Semantics. ^_^ The promise can represent a computation, which can hold some releasable resources.
To my mind this is an important distinction. The promise does not represent the computation. It represents the result.
Any representation of the computation itself bring in a whole lot baggage which includes, the problem of managing distributed computation in the cloud plus a lot of other issues. Dragging this stuff into the base level of a programming language is a bad crossing of abstraction levels.
Cheers - Bill
Bill Frantz | When it comes to the world | Periwinkle (408)356-8506 | around us, is there any choice | 16345 Englewood Ave www.pwpconsult.com | but to explore? - Lisa Randall | Los Gatos, CA 95032
On Wed, May 1, 2013 at 9:38 AM, Jonas Sicking <jonas at sicking.cc> wrote:
On May 1, 2013 8:21 AM, "Tab Atkins Jr." <jackalmage at gmail.com> wrote:
On Wed, May 1, 2013 at 12:16 AM, Jonas Sicking <jonas at sicking.cc> wrote:
However with this approach you get an API which automatically simply works as an API returning a Future in case you don't need to abort the operation or display its progress:
handleResult(doSomeOperation()); or return doSomeOperation(); or doSomeOperation().then(val => displayResult(val));
I.e. if you don't care about the operation part, the API simply works as any other API which returns a promise. This seems like a very nice thing. The only "cost" of this API is that it doesn't compose when you compose the future, but neither does the dispose object in the tuple approach.
The other cost is an inherent capability leak - unless you purposely strip it of its cancelability, passing it around to anything else gives the "anything else" the ability to cancel your promise as well. The tuple approach doesn't have this issue - the promise and the canceler/resolver are inherently separated, and you have to purposely hand the canceler/resolver off to other code for it to have any powers.
This is a problem that the current XHR API is suffering from too. Has that been a problem in reality?
With XHR you can even affect the values that other consumers are receiving by setting .response type. Again, does anyone have examples of when this has been a problem?
I feel like attempting to solve this problem will mean that we need to split a lot of interfaces into lists of objects, each representing a capability. This seems like it will very quickly lead to an unmanageable mess.
I don't think this is a valid slippery-slope argument. If it were true, it would show itself more strongly, as us, for example, splitting up the resolver into two or three different objects for each of the operations, and maybe the promise into two objects as well for each of the channels.
In practice, you can split things into capabilities that are reasonable to group together. However, a cancellable promise blurs the line. Maybe that's not important? I dunno.
Wrapping a CancelableFuture in a normal Future seems like an acceptable solution which only adds complexity in the much more rare case that someone cares about capabilities.
If we knew that chaining a CancelableFuture always returned a normal Future, then this might be okay - just do "getCancellableFuture(foo).then()" to get a normal future out.
On Wed, May 1, 2013 at 11:04 AM, Bill Frantz <frantz at pwpconsult.com> wrote:
On 5/1/13 at 7:54 AM, jackalmage at gmail.com (Tab Atkins Jr.) wrote:
On Wed, May 1, 2013 at 5:50 AM, Bill Frantz <frantz at pwpconsult.com> wrote:
On 4/30/13 at 11:04 PM, jackalmage at gmail.com (Tab Atkins Jr.) wrote:
...
- Some promises are holding onto resources (locks, network connections, cpu time) which will be disposed of when they're finished.
I think this statement is wrong. Promises don't hold resources. They are a placeholder for a value to be provided later. Perhaps the computation which may provide the value at some future time holds a resource, or the computation which will consume the value when it is resolved holds a resource (generally a poor programming practice), but the promise itself doesn't hold resources.
Semantics. ^_^ The promise can represent a computation, which can hold some releasable resources.
To my mind this is an important distinction. The promise does not represent the computation. It represents the result.
Any representation of the computation itself bring in a whole lot baggage which includes, the problem of managing distributed computation in the cloud plus a lot of other issues. Dragging this stuff into the base level of a programming language is a bad crossing of abstraction levels.
I think you're making this far too complicated. It's much simpler than this:
- XHR is a very reasonable API to Future-ize.
- XHRs are cancellable.
- Ergo, we should have a cancellable Future subtype.
I think you're making this far too complicated. It's much simpler than this:
- XHR is a very reasonable API to Future-ize.
- XHRs are cancellable.
- Ergo, we should have a cancellable Future subtype.
Curious: has this proposed XHR api been published anywhere?
On Wed, May 1, 2013 at 11:17 AM, Kevin Smith <zenparsing at gmail.com> wrote:
I think you're making this far too complicated. It's much simpler than this:
- XHR is a very reasonable API to Future-ize.
- XHRs are cancellable.
- Ergo, we should have a cancellable Future subtype.
Curious: has this proposed XHR api been published anywhere?
I thought it was at slightlyoff/DOMFuture, but
looks like it doesn't include an XHR example reworking yet.
For an example, though, just look at something like jQuery's API, but with the callback arguments moved to a returned future. (Or jQuery's deferred-based API.)
From: Tab Atkins Jr. [jackalmage at gmail.com]
I think you're making this far too complicated. It's much simpler than this:
I disagree. An abstraction boundary gets broken. Consider:
function getUser1() {
return doXHR("/user.json");
}
function getUser2() {
return { "name": "domenic" };
}
function getUser3() {
return doXHR("/user.json").then(function (user) {
user.metadata = "<meta> tags are old school";
});
}
Here, getUser1()
returns a cancellable promise, but getUser2()
does not, even though they should have the same semantics. Worse, getUser3()
isn't cancellable, even though it was originally derived from an XHR. Trying to fix the getUser3()
case is what takes you down the slope of complex additional semantics.
Much better would be if a hypothetical future XHR utility returned a { promise, abort }
object, with no logical connection between the abort
and the promise
(besides maybe rejecting the promise with a well-known error). This seems much better than trying to make a general cancellation concept for promises and overloading them with additional semantics about the operation being performed.
This is where something like an external cancellation source could be more effective:
function getUser1(cancelToken) {
return doXHR("/user.json", cancelToken);
}
function getUser2(cancelToken) {
// immediate result, no need for cancellation
return { "name": "domenic" };
}
function getUser3(cancelToken) {
return doXHR("/user.json", cancelToken).then(function (user) {
user.metadata = "<meta> tags are old school";
});
}
var cts = new CancellationTokenSource();
var usersF = Future.every(getUser1(cts.token), getUser2(cts.token), getUser3(cts.token));
usersF.done(function(users) {
});
cts.cancelAfter(5000); // timeout after 5 seconds.
Rather than subclassing Future, you use an external source provided by the caller to manage cancellation. Now it doesn't matter whether either of the 3 methods have a return value that supports cancellation, and if the caller doesn't need to cancel, it can provide null or undefined.
Ron
On 5/1/13 at 11:13 AM, jackalmage at gmail.com (Tab Atkins Jr.) wrote:
I think you're making this far too complicated. It's much simpler than this:
- XHR is a very reasonable API to Future-ize.
- XHRs are cancellable.
- Ergo, we should have a cancellable Future subtype.
Why make it more complex than necessary. While a XHR implementation may wish to add a cancel operation, JS is a broader language than just the web. There are use cases that don't need cancel and they should not have to pay the costs of the additional communication paths that cancel will require.
With a simple promise, others can build objects which use the promise as an internal component and provide cancel or other useful operations. Leaving the implementations of these other operations to libraries will allow experimentation to proceed standardization.
Cheers - Bill
Bill Frantz | Since the IBM Selectric, keyboards have gotten 408-356-8506 | steadily worse. Now we have touchscreen keyboards. www.pwpconsult.com | Can we make something even worse?
On Wed, May 1, 2013 at 1:29 PM, Bill Frantz <frantz at pwpconsult.com> wrote:
On 5/1/13 at 11:13 AM, jackalmage at gmail.com (Tab Atkins Jr.) wrote:
I think you're making this far too complicated. It's much simpler than this:
- XHR is a very reasonable API to Future-ize.
- XHRs are cancellable.
- Ergo, we should have a cancellable Future subtype.
Why make it more complex than necessary. While a XHR implementation may wish to add a cancel operation, JS is a broader language than just the web. There are use cases that don't need cancel and they should not have to pay the costs of the additional communication paths that cancel will require.
With a simple promise, others can build objects which use the promise as an internal component and provide cancel or other useful operations. Leaving the implementations of these other operations to libraries will allow experimentation to proceed standardization.
Ah, I'm not proposing that we augment the base Future class with cancellation properties. I explicitly used the term "subtype" in the quoted bit above. Some of Ron's suggestions were to augment the base Future class, but not all of them, and several other people pushed back on that.
On 5/1/13 at 1:37 PM, jackalmage at gmail.com (Tab Atkins Jr.) wrote:
Ah, I'm not proposing that we augment the base Future class with cancellation properties. I explicitly used the term "subtype" in the quoted bit above. Some of Ron's suggestions were to augment the base Future class, but not all of them, and several other people pushed back on that.
I think that covers the issue.
Cheers - Bill
Bill Frantz | Re: Computer reliability, performance, and security: 408-356-8506 | The guy who is wearing a parachute is not the www.pwpconsult.com | first to reach the ground. - Terence Kelly
On Wed, May 1, 2013 at 7:17 PM, Kevin Smith <zenparsing at gmail.com> wrote:
Curious: has this proposed XHR api been published anywhere?
My tentative plan is to have a new API that sits on top of fetch.spec.whatwg.org somehow. (In due course xhr.spec.whatwg.org will build almost directly on that too, so the underlying architecture would be shared across the platform.) E.g.
fetch(Request) that returns a Future<Response>. Where exactly "fetch"
should be and what shortcuts we should provide, etc. is not entirely clear to me yet.
On 5/1/13 at 5:43 PM, frantz at pwpconsult.com (Bill Frantz) wrote:
On 5/1/13 at 1:37 PM, jackalmage at gmail.com (Tab Atkins Jr.) wrote:
Ah, I'm not proposing that we augment the base Future class with cancellation properties. I explicitly used the term "subtype" in the quoted bit above. Some of Ron's suggestions were to augment the base Future class, but not all of them, and several other people pushed back on that.
I think that covers the issue.
Cheers - Bill
Upon further reflection, I think the point about premature standardization stands.
Also, a pure inheritance model -- which is implied by subclassing -- may be the wrong model. If one wants to include POLA in one's programming style, one will probably want to separate the authority to access the resulting value from the authority to abort the computation.
Cheers - Bill
Bill Frantz | gets() remains as a monument | Periwinkle (408)356-8506 | to C's continuing support of | 16345 Englewood Ave www.pwpconsult.com | buffer overruns. | Los Gatos, CA 95032
On Thu, May 2, 2013 at 1:40 PM, Bill Frantz <frantz at pwpconsult.com> wrote:
Upon further reflection, I think the point about premature standardization stands.
FWIW, that answer does not work for the web. If people want a particular API it will be made, shipped, and network effects will prevent us from ever removing it. So we should try to figure out some answers here. In hindsight some of those decisions may/will turn out to be wrong, which will help us doing make better decisions going forward. If we find out soon enough we might be able to rectify a few things.
There is a clear desire for lots of new platform APIs that will be shipping soon (or are shipping already) and for those platform APIs to be better than those created thus far. Futures may not be perfect, but do represent a huge improvement over the options available to date. It follows they'll be used and adapted as needed. We can either choose to influence that or have others make the decisions for us.
Also, a pure inheritance model -- which is implied by subclassing -- may be the wrong model. If one wants to include POLA in one's programming style, one will probably want to separate the authority to access the resulting value from the authority to abort the computation.
I've created separate gists for three different ways that I am currently investigating as a means to support the cancellation of a Future. These can be found here:
Each has a list of some of the benefits and issues I've seen while experimenting with each approach, as well as possible changes to the various APIs or algorithms for Future to make each happen.
In general, cancellation of a Future can be beneficial in a number of cases. One example is the case where you are requesting a resource from a remote server using XHR. If the request was being made to fetch a page of data, and the user opted to move to the next page before the current page completed loading, it no longer becomes necessary to continue fetching the remote resource. In addition, it is no longer necessary to handle any additional computation or transformation logic that would have resulted from the successful completion of the fetch operation. Having the ability to cancel the request allows an application to quickly release resources that it no longer needs.
It is also useful to be able to handle the cancelation of a long running task that might be executing in a Worker. In this case, cleanup logic that is part of cancelation would request the worker to close, ending the current operation and releasing resources.
Both of the above examples are indicative of cancelling the root of an operation, but there are also circumstances where you might want to cancel a chained Future and any Future chained from it, without canceling the root. In the previous example regarding paged data, I might wish to allow the fetch operation to complete so that I could cache the data for quick retrieval, but would only want to cancel any possible UI updates that might occur in a chained Future.
I'm interested to hear what others think with respect to properly handling cancellation with Futures.
Ron