-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Don't use a third "cancelled" state #565
Comments
In fact this sounds reasonable especially together with cancel request counting on branching. |
I've finally undersood what @bergus is proposing. All pending upstream promises (where Instead of running onReject/catch handlers they should run their onCancel handlers (synchronously with initial .cancel()) - in natural order, not an inverse (like it is implemented at the moment). All exceptions that are thrown in onCancel handlers should be rethrown globally (process panic). After all onCancel were executed After reaching the cancellation point, it should be turned into normal rejection with the same CancellationError for the downstream. So here we see effective
|
UPD: updated previous comment. @petkaantonov what will be your verdict? I personaly starting to believe that the described behaviour is better and more correct that the implemented one (3.0) (sorry!) |
@petkaantonov, once you have found the courage to abandon the chosen design. Maybe now is the case when it worth to do it again? Proposed changes will not affect those who use cancellation properly
All othets are in potential trouble with the current design. And what proposes @bergus, looks like good point to stop and rethink/discuss it twice before publishing 3.0 as the proposed semantics is correct (IMO). Anyway, Happy Easter :) |
|
It is true that one could potentially observe a cancelled state by doing something like: var thenCalled = false;
.then(function() {
thenCalled = true;
}, function() {
thenCalled = true;
}).finally(function() {
if (!thenCalled) {
// Got here because of cancellation
}
}); And of course with However doing these should feel wrong enough (at least for the first case, .reflect is just reflecting) that people are not encouraged to react to cancellation as third state downstream. |
@petkaantonov I think that non-propogating onCancel to the downstream without rejecting the downstream breaks both designs. Like @bergus I think that forget semantics should apply only on the path between cancellation point up to the farthest parent with single subscriber. On that way all promises even 'reflect' ones should be internally sealed as rejected for any subscribers that may be attatched to these promises later. The downstream should also should be treated as attatched later thus should be rejected. Implemented cancellation now is infectious. If you touch it - you become infected with cancellation state. If you chose not to propogate onCancel downstream only, without rejecting the downstream and leaving all the graph in the infectious state then the state of the program becomes inconsistent. I will provide you a contra-example for this (on the evening - i'm on the trip and writing from my phone). @bergus - where are you? correct me if I'm wrong |
Looks okay to me. But I do have a question. What should I write in order to make The API is: db.transaction(tx => tx.query(...).then(_ => tx.query(...))) and the current implementation is: function transaction(f) {
Promise.try(connectionPool.begin)
.then(tx => Promise.try(f, tx)
.then(res => tx.commitAsync().thenReturn(res),
err => tx.rollbackAsync().catch(ignore).thenThrow(err)))
} If I understand the new cancellation correctly, now I just need one more handler to handle the case where the promise created by Promise.try(f, tx)
.onCancel(_ => tx.rollbackAsync().catch(ignore))
.then(res => tx.commitAsync().thenReturn(res),
err => tx.rollbackAsync().catch(ignore).thenThrow(err)) and this will correctly clean up the transaction in every single case? But if the propagation is upstream only, it doesn't seem like the transaction will get cleaned up at all? |
@spion Cancellation is like long stack traces, it needs to be enabled before any promises are created in configuration. Since you don't use it you don't need to handle it at all. |
@petkaantonov this is a library function - what if a user of my library enables cancellation on their copy of bluebird for their needs? |
Then you need to use |
Can I just add a finally handler? Promise.try(f, tx)
.then(res => tx.commitAsync().thenReturn(res),
err => tx.rollbackAsync().catch(ignore).thenThrow(err))
.finally(_ => tx.isOpen() && tx.rollbackAsync()) |
That works too, finally is always called |
Yes @Artazor I'm glad you understood me :-) I especially like your formulation
Exactly what I had in mind, but could not get into words… "Non-infectious" cancellation and "sealing cancelled promises as rejected for other downstream subscribers" sound good as well. Happy easter! |
I'm not sure about the order of cleanup/cancellation callbacks, though. Assuming
is there any order the callbacks need to be called in? Do users expect a specific order? My own library just runs upstream on the chain and calls |
@bergus c a b d: .onCancel isn't part of the chain at all (and the second onCancel shouldn't even be possible if it could be enforced), in fact I would replace it with third argument to constructor if it was feasible (bluebird is used primarily through promisified functions that already return promises). |
Ok I will make onCancel only available inside constructor since the room for misunderstanding is too big and cannot be non-hackily enforced with an external method |
Ah I see what you mean, |
It's also entirely optional optimization, nothing should be different when you don't actually |
@petkaantonov, the decision to expose onCancel in promise constructor is reasonable and logical since in most cases this is the right place to define the aborting logic. However, it may introduce unnecessary function loadResource(resource) {
var loader = new FancyLoader(resource.name, {
loading: 'loading.mpg',
aborting: 'aborting.mpg',
ready: 'ready.jpg',
failed: 'failed.jpg'
});
form.add(loader); // plays 'loading.mpg' to the end user (assuming initial state == 'loading')
return requestWithAsyncCleanupLogic(resource.url).onCancel(function(){
loader.setState('aborting'); // plays 'aborting.mpg' to the end user
}).finally(function() {
// the time passed between cancellation and this handler may be significant
// since the request being cancelled has an asynchronous cleanup (promises in finallies)
if (loader.isInState('aborting')) {
form.remove(loader);
}
}).then(function(result) {
loader.setState('ready');
loader.setDataPreview(result);
return result;
}, function(reason) {
loader.setState('failed');
loader.setDataPreview(formatError(reason));
throw reason;
});
} I just wanted to demonstrate that there are valid use cases for public
|
It's not too bad: return new Promise(function(resolve, _, onCancel) {
resolve(requestWithAsyncCleanupLogic(resource.url));
onCancel(function(){
loader.setState('aborting'); // plays 'aborting.mpg' to the end user
});
}) Instead of return requestWithAsyncCleanupLogic(resource.url).onCancel(function(){
loader.setState('aborting'); // plays 'aborting.mpg' to the end user
}); Of course most people will probably manually wire up the reject instead of taking advantage of resolve |
Or the mentioned example is exactly the case you wanted to prevent? Was it prohibited to use .onCancel to expose the fact of cancellation to the outer world, and the only legitimate use for .onCancel was
that should be non-observable except via the performance change? |
Oh, I've just seen your comment |
And this will be catastrophic for them since the following code return new Promise(function(resolve, reject, onCancel) {
requestWithAsyncCleanupLogic(resource.url).then(resolve, reject);
onCancel(function(){
loader.setState('aborting'); // plays 'aborting.mpg' to the end user
});
}) is definitely an antipattern and is not equivalent to your example. Besides it will break the cancellation propagation to the upstream. Won't it? |
@Artazor yes if requestWithAsyncCleanupLogic had onCancel as well, it wouldn't be called |
The correct version (alas! still smells like an anti-pattern) of manual wiring up all handlers will be: return new Promise(function(resolve, reject, onCancel) {
var p = requestWithAsyncCleanupLogic(resource.url).then(resolve, reject);
onCancel(function() {
loader.setState('aborting'); // plays 'aborting.mpg' to the end user
p.cancel();
});
}); |
I'm apologising for my English as I'm really not confident with my ability to express long thoughts in it. May be in other circumstances it would be better to meet in a video/chat, but my verbal English is even worse than written, and I don't expect that it will be comfortable to you. From the clash of opinions emerges the truth. I'll try to shatter your confidence in chosen design once more with the following sequence of observations:
_to be continued..._ |
@Artazor this is great, I eagerly await your updates/continuation :) edit: you might want to publish this as a blog post as well. By the way your English is excellent and it seems to me that with a few extra passes at the piece you will arrive at a nicely polished article on cancellation |
So for multi branched promises, a promise should have its If on top of this, you need to somehow swoop in and attach a new branch to a promise that has been cancelled, the newly branched promise should just be rejected with cancellationerror? And that's it? We have sane promise cancellation so easily? |
Just an idea: it would be nice to come up with 2 or 3 different use cases and see how different designs would apply to them. |
Here is a simple use case: A series of two XHRs are sent to the server to fetch search results: the first one fetches IDs matching the search box text, the next one returns a dictionary of actual objects that correspond to those IDs. At the same time a spinner is activated to indicate that an operation is in-flight. There are two possible scenarios:
In both situations the spinner should disappear at the end. How would the code look like in this design? More interestingly, how would it look on the back end? (cancellation is caused by the HTTP connection being closed; every operation looks like this: allocate transaction, then perform a series of queries within it) |
@spion - yes, I've planned to demonstrate pros and cons of different designs on several concise yet realistic use cases. Just need more time to prepare the materials (I'll include your example)
roughly - yes. In details - no. Anyway, there is also a room for uncertainty. I'll try to cover all my considerations today in the late evening (after ~23:00 EEST)/tonight. The design that I'm investigating at the moment involves the following:
Maybe phases 1 and 2 can be performed in one pass.
Arrrrgh, without simple graphical representation it is too hard to explain it by words |
@Artazor thanks for the comprehensive reading (and your English is definitely no worse than mine!). I'm unsure about the terminology for "single consumer case", though. In this, a promise on its own could still have multiple consumers, couldn't it? The "branches are merged back" case is quite common imo. You relate this to structured programming, I would think a "single exit point" matches this better "branching is harmful". But actually none of this describes parallel execution well (where we want to spread the flow), so I'd rather use the phrase open (promise chain) ends considered harmful. @petkaantonov Yes, that's it :-) So sane, so simple!
but I guess those tokens have confused everyone who was comfortable with the .net CancellationTokenSource model. If Bluebird uses this in 3.0, I'm looking forward to see how you implement the ref-counting and callback-cancelling efficiently. Also, I'd love to get this behaviour constituted in a cancellation specification that also provides a way to make cancellation interoperable (in the thenable assimilation procedure). |
Well, I don't :-D. Cancellation is already behind a switch because it takes 3 additional fields on the promise + new closure needs to be allocated for every |
Oh I didn't see your latests posts. @Artazor
I would not make this "barrier" an argument to the To implement a "barrier", I thought of a This is based on an algorithm a bit different from yours: sealing and notifying are merged into one phase. A |
@bergus - you are right about terminology. Of course, the branching itself is not harmful as soon as it balanced with merging back and providing that there eventually will be a single exit point (exactly like in structured programming). So I'll use your open (promise chain) ends considered harmful and the "single exit point" in upcoming comments/updates (as well as when/if our adventure will be documented as a blog post :) About barrier - I will show the drawbacks of 'uncancellable()'. Anyway, I know at least three different approaches how to deal with that problem (hope to cover them as well). |
@spion Here is what I would do: function fetch(url, data) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open("get", url, true);
xhr.onerror = reject;
xhr.onload = function(e) { resolve(this.response); };
xhr.responseType = "json";
xhr.send(data);
return function onCancel(err) {
xhr.abort();
};
});
}
function next(target, type, predicate) {
return new Promise(function(resolve) {
function listener(e) {
if (!predicate(e)) return;
fin();
resolve(e);
}
function fin(err) {
target.removeEventListener(type, listener);
}
target.addEventListener(type, listener, false);
return fin;
});
}
var search = document.getElementById("searchbox");
search.addEventListener("change", function(e) {
var esc = next(search, "keydown", functon(e) { return (e.keyCode || e.which) == 27; });
var result = fetch("/search-ids", search.value).then(function(ids) {
return fetch("/dictionaries", {ids: ids});
});
showSpinner();
var display = result.finally(hideSpinner).then(showResults, showError);
Promise.race(esc, display); // the first one cancels the other
// alternatively:
Promise.race(esc, result); // similar, but hitting esc invokes `showError`
// alternatively:
Promise.race(esc.then(showCancelMessage), display)
// alternatively:
var hasCancelled = esc.then(function() {
result.cancel(new Error("message")); // invokes `showError` with custom argument
});
result.finally(function() { hasCancelled.cancel(); });
}, false); Of course, cancellation is complicated, and not using |
Guys, I'm sorry, seems that update will be in next two days! |
@Artazor any update? :p |
Sorry! I've tried to build a formal framework for reasonong about cancellation and seems dig too deep. |
@Artazor any news? :D |
@spion, you dont know how close to the truth this picture is. I hope I'll publish my investigation and the spec for cancellation on saturday/sunday. But yes, I've gone deeper and constructed other entity very close to Promises/A+ (in fact almost all specs hold but in generalised form) and it lacks almost all problems that come with promises, including possiblyUnhandled rejection (it is impossible to to miss the exception or it never really happenedhappened), and it has consistent cancellation, and other useful properties. So my cancellation semantics will be simple "downporting" to Promises/A+. By the way my "Promises" are more lightweight and scheduler independent (however all async-related formal properties are not violated). The only thing I'm afraid of that they are already invented. -) |
@Artazor just make sure you don't give up on anything fundamental promises are awesome at:
;) |
@benjamingr Maybe off-topic here, but what exactly is so awesome about eagerness? |
@bergus, are you clairvoyant? :D In my model Thunks/A+ I have lazy '.thus' that is lazy analog of 'then' and 'then' in my model has role of 'done' which converts a chain into real eager promise. But you should not call 'then' often (just as done in bluebird). But yes, I think it is an offtopic. |
Yup, definitely reinventing. |
(please tag 3.0)
It seems that #415 established that there should be no extra "cancelled" state. However, the current implementation seems to do just that - it settles the promise, and propagates this downstream, calling only
onCancel
andfinally
handlers but noonFulfilled
oronRejected
ones. E.g. in the test case described in #564, where athen
callback returns a cancelled promise that is subsequently adopted and triggers a downstreamonCancel
.For me, a cancellation should only propagate upstream, where it prevents fulfillment/rejection callbacks from being invoked, stops ongoing tasks and cleans up resources (by triggering
onCancel
andfinally
callbacks).The cancellation should not propagate downstream. Instead, rejection with a CancellationError (for those who peek, usually
.cancel()
is called at the end of the chain anyway) seems appropriate here.The text was updated successfully, but these errors were encountered: