A year or two ago, I brought this issue up with Mark Miller in a chat.
He strongly discouraged tacking this functionality onto promises. The
notion is that promises, when used as a security abstraction,
guarantee that information can only flow in one direction. So you can
safely give a promise to a gaggle of mutually-suspicious consumers and
give the corresponding resolve function to a skein of
mutually-suspicious providers. With a more rigorously
security-conscious implementation of Q than mine, you are then
guaranteed that only one provider will be able to send one message to
a consumer. All other providers can call resolve all they want and
none of them will know which of the others successfully sent their
value. All of the consumers can observe the promise and are equally
unaware of each-other or where the value came from.
When we introduce cancellation to promises, it is then possible for a
consumer to communicate with a provider, a tiny message. But a one-bit
serial channel of information is sufficient to communicate.
I am at least convinced in my own work that we need to be able to
cancel promises, but I am not sure how to go about it without losing
good properties. It might be possible to bifurcate the system into
secure and insecure components with an identical API. In that case,
abort messages might get intercepted at some gate and discarded to
prevent cross-talk.
Perhaps Mark Miller can weigh in on this in detail.
Kris
I took a stab at a cancelable "delay" call (clearTimeout of the
setTimeout) with decent success in the trivial case, but not in the
case of canceling a derived promise, which would need to propagate
that disinterest backward.
I should bring up that Mark Miller told me a year ago to not pursue
cancelable promises because it would break a fundamental security
property of promises: that information can and should only travel one
direction: from the resolver to the promise, not from promise to
promise and not from resolver to resolver. Deciding to ignore
subsequent calls to deferred.resolve() rather than throwing an error
was a decision guided by Tyler Close to prevent information from
flowing from resolver to resolver. Cancellation allows information to
flow from promise to promise, backward. I still don’t fully understand
the argument, so that is as good as I can tell. In any case, Mark
suggests that cancellation should be accommodated by another means.
I think that implies that any cancelable action would need to return
{promise, cancel} objects instead of simply returning promises, which
is not exactly ergonomic. In any case, cancellables are frequently
requested, I have wanted them, and I know that the NodeJS core team is
also looking into cancellation: they are considering putting their old
brand of promises (to be given another name like Handle to avoid a
holy war) back in core to facilitate this, e.g.,
var stat = FS.stat("foo.txt", cb(error, value)); // to retain compatibility
stat.cancel();
var stat = FS.stat("foo.txt");
stat.on("success", cb(value));
stat.on("error", eb(error));
stat.cancel();
I’ve already intimated to Isaac that event emitters could be trivially
made into thenables, which would make it possible to trivially
assimilate them in Q.
Handle.prototype.then = function (cb, eb) {
this.on("success", cb);
this.on("error", eb);
};
Q.when(FS.stat("foo.txt"), cb, eb);
Anyhow: cancellation. Keep thinking. I will too.
Kris Kowal
---
More concretely, we had one system like the following:
var promise = downloader.downloadAsync("book id");
// ... later, in response to UI action
downloader.cancelDownload(promise);
This was implemented inside downloader by keeping a collection of cancelled
promises, and upon completion of the download, only resolving/rejecting the
corresponding deferred if that collection did not contain that deferred's
promise.
---
Another, somewhat different use case involved offering the user a dialog
with some choices:
showDialogAsync().then(
function (choice) {
// process choice
},
function (error) {
alert("Server sided error retrieving choices to display");
}
);
If the user simply clicked "Cancel" or the close button, we wanted neither
of these handlers to run. This was solved by simply never resolving or
rejecting the deferred, but I suppose that means we have some
waiting-for-resolution promises floating around in memory. Telling the
deferred "we don't care anymore" might help, memory-wise. (I suppose the
same concerns apply to my previous example.)
---
In general I think deferreds are the right place to inject cancellation
functionality. Since an API doesn't normally expose deferreds directly, the
security guarantees are not lost in the normal case. If the promise-producer
wants to expose cancellation functionality in a specific case, he can do so,
either via a separate method like the downloader example above, or perhaps
by just doing deferred.promise.cancel = deferred.cancel before returning
deferred.promise. This also allows case-by-case injection of additional
cancellation code into the exposed cancellation method.
Thus the API change would simply be the addition of deferred.cancel(), with
the following semantics:
* If the deferred is already resolved or rejected, noop.
* Otherwise, guarantee that future calls to deferred.reject and
deferred.resolve are noops.
This seems somewhat similar to jQuery 1.7's $.Callback.prototype.disable(),
where $.Callback appears to be the combination of a fuzzy understanding of
promises with an active imagination.
Best,
-Domenic
The purpose I have in mind for cancellation is to prevent large
amounts of work to be wasted. For example, if I have requested a
stream of 1000 records and I then discover I am interested in a
completely different set of 1000 records, and perhaps this interest is
a very wild and varying thing, I will want to cancel things
frequently. If this is the case, guaranteeing that callbacks are not
called is merely a side-effect of disinterest. However, the point
about needing to prevent callbacks is interesting.
Kris Kowal
In my proposal this would be achieved with something like
https://gist.github.com/1500248 or, perhaps less securely but more
concisely, https://gist.github.com/1500255
-----Original Message-----
From: q-con...@googlegroups.com [mailto:q-con...@googlegroups.com] On
Behalf Of Kris Kowal
Sent: Monday, December 19, 2011 23:16
To: q-con...@googlegroups.com
Subject: Re: [Q] Loss of interest in promise resolution + aborting deferreds
Suppose that you have a memoized function that returns a promise.
var foos = {};
function foo(a) {
if (!foos[a]) {
foos[a] = fooActual(a);
}
return foos[a];
}
In this case, multiple observers will receive the same promise object.
These observers should not have the power to interfere with each
other.
Meaning, if Alice calls foo(1) and then Bob calls foo(1).cancel()
before Alice’s promise is resolved, work must not be cancelled such
that Alice still receives foo(1)’s resolution. For the purposes of
this example, Charlie is out to lunch.
Given that every call to "foo", in this very common pattern, returns
the same result, it is impossible to distinguish Alice and Bob’s need
for resolution, so as such cancellation necessarily applies to both.
This could perhaps be mitigated by explicitly rewrapping the returned
promise for each individual consumer, but at best, cross-cancellation
would become a hazard that people would have to attend to.
Kris Kowal
In many instances promises will not be shared among more than one observer,
so a simple deferred.promise.cancel = deferred.cancel might suffice. But for
ones where that is expected, the promise-producer would need to (manually)
implement reference counting of some sort.
-----Original Message-----
From: q-con...@googlegroups.com [mailto:q-con...@googlegroups.com] On
Behalf Of Kris Kowal
Sent: Monday, December 19, 2011 23:33
To: q-con...@googlegroups.com
Subject: Re: [Q] Loss of interest in promise resolution + aborting deferreds
Each call to the `then` method must produce a promise with a `cancel` method that remembers the callbacks that were applied with that `then`. The `cancel` method returns a promise that the callbacks will not be called.
Are you talking about the errback of other agents or of the agent that induced the cancel?
I don't like the idea of a global cancel for a promise. That does create security leaks.
What is wrong with the 'cancellation' being purely for agents to say they are no longer interested in having their callback or errback called and it being up to the promise provider to determine whether or not to actually 'cancel' the operation based upon reference counting or other heuristics?
For what it’s worth, Q-Comm is overdue for a rewrite and I failed to
make this distinction because I did not know how to make it work. I’ll
make a study of makeQ and see what I can do on the next iteration. On
a cursory examination, the internal mechanisms do not have obvious
analogs, so it might take some time.
> As an example of the power of the Q approach (whether makeQ.js, ref_send, or
> qcomm), building on makeQ, at
> <http://code.google.com/p/google-caja/source/browse/trunk/src/com/google/caja/ses/makeSimpleAMDLoader.js#98>
> I implement a subset of the AMD module loader API in almost no code at all.
> On an adequately ES5 conformant browser, if you visit
> <http://google-caja.googlecode.com/svn/trunk/src/com/google/caja/ses/explicit.html>
> and the last line says "AMD loader test...succeeded" then the example above
> seems to work. However, the testing to date has been very light. Are there
> any extensive Q test suites I might be able to adapt?
Irakli Gozashvili made extensive CommonJS tests for Q. Like all test
suites, some adaptation will likely be necessary both of the scaffold
and of the validity of the tests given our modest divergence.
> I would like to better understand the differences between makeQ, ref_send,
> qcomm, bcap <https://sites.google.com/site/belayresearchproject/bcap>, and
> caja-captp <http://code.google.com/p/caja-captp/>. I would like see makeQ
> and Dr.SES eventually able to interoperate with more of these. Even better
> would be to converge more of the advantages of these various systems into
> one library.
For sure. I can try to write up a list of divergences while I read
makeQ. One is that my Q.all accepts an array instead of being
variadic. We are also rolling with thenables because that property
allows all of our libraries to adapt to each other. Even jQuery, which
has so little in common with our implementations, can still be adapted
because it is compatible with the minimum thenable contract. For what
it’s worth, my Q promises are also whenable to attempt to retain
compatibility with the concurrency strawman.
We also have news. Nathan Stott just posted a new article on promises,
highlighting the interoperability between my fork of Q and Kris Zyp’s
PromisedIO, which I believe is also Dojo’s Deferred.
> As Kris indicates, in makeQ I am taking a very principled stance on
> information flow, as Tyler does in ref_send. Later I hope to find the time
> to address the concerns with cancellation which have been raised in this
> thread. My belief, yet to be demonstrated, is that these concerns can be
> addressed well with simple patterns involving multiple promises.
I feel more and more like I’m going back and forth between shoving a
square block into a round hole and a round block into a square hole.
We need to either find a safe way to make the existing patterns
cancelable or find a way to make pairing cancelers and promises
somehow beautiful enough that it is suitable to use such pairs
everywhere promises are bought and sold.
In a similar vein, Dojo and jQuery the stance that a deferred
conflates a promise and a resolver, which breaks the principle of
least authority and one-way information flow so desperately preserved
in makeQ and ref_send (which I’ve striven for API compatibility with
in Q). They both however provide means to separate the promise and
resolver if at any point you explicitly distrust your consumer (which
is an anti-pattern in the security business; secure should be
default). I am sure it would be suspicious to apply the same
anti-pattern to solve another problem, but we could implicitly
conflate cancellation with a corresponding promise and then explicitly
separate the two with a function call, perhaps with a .nocancel()
call, which would return an object of the same form but a noop
.cancel(). If we did this, it would become the responsibility of every
promise provider to explicitly decide whether cancellation will be
possible and how it would propagate whenever they return a promise to
another consumer.
Kris Kowal
For the record, when Nathan mentions “promise theory”, he is talking
about it in the context of game theory applied to creating stable
policies among selfish autonomous agents in a distributed system. He
sent me this paper yesterday.
http://project.iu.hio.no/papers/pcm.2.pdf
Kris
It is not necessary for cancel to return a promise in order for the
provider to not be obliged to obey. They can already observe the
outcome by watching the resolution of the promise. It may be nice to
disable the original promise and take over with a returned
cancellation promise.
Consider: all promises have a .cancel method that by default returns a
rejected promise. If someone wants to explicitly handle cancellation,
including propagation of cancellation, they must call
.cancellable(handler) on the promise they provide. My previous
example of a memoized function remains correct with no information
backflow, but it can be revised to add the cancellation feature;
var foos = {};
var refcounts = {};
function foo(a) {
if (!foos[a]) {
refcounts[a] = 1;
foos[a] = fooActual(a);
} else {
refcounts[a] += 1;
}
return foos[a].cancelable(function () {
refcounts[a] -= 1;
if (refcounts[a] == 0) {
foo[a].cancel(); // propagate back, probably a no-op
}
});
}
But maybe that's the difference between a cancellation and an abandonment.
That’s in the air. I am presently analyzing the case of memoization in
the context of inadvertent information flow. To prevent a leak, it
would at least be necessary for cancelation to always be treated as a
success. That means that the fulfillment handler must never be called.
If the rejection handler is called, it must be called without delay,
so that the canceller cannot observe whether there were other
subscribers.
That is what is necessary. It would certainly be sufficient for
.cancel() to return undefined and prevent the promise from calling any
further handlers, regardless of whether the cancelation were a
success. Thus, leaving the caller of cancel with the responsibility of
tying up any loose ends.
I am steering in that direction.
Kris Kowal
define(['q'], function (Q) {
'use strict';
// Adds a .release method on the promise that basically
Q.makePromise.prototype.releasable = function () {
var deferred = Q.defer();
this.then(function (value) {
if (!deferred.promise._released) {
deferred.resolve(value);
}
}, function (reason) {
if (!deferred.promise._released) {
deferred.reject(reason);
}
}, function (progress) {
if (!deferred.promise._released) {
deferred.notify(progress);
}
});
deferred.promise._releasable = true;
return deferred.promise;
};
Q.makePromise.prototype.release = function () {
if (!this._releasable) {
throw new Error('Promise is not releasable');
}
this._released = true;
return this;
};
return Q;
});
this._promise = sdk.getSomething().releasable()
this._promise.then(function () {
that._content.render({ dsView: dsView });
})
.done();
// When I'm no longer interested:
this._promise && this._promise.release()
--
You received this message because you are subscribed to the Google Groups "Q Continuum (JavaScript)" group.
To unsubscribe from this group and stop receiving emails from it, send an email to q-continuum...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.