-
Notifications
You must be signed in to change notification settings - Fork 165
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
Rephrase next-event-loop-turn requirement. #104
Conversation
This fixes #100, by explicitly calling out the concept of an event loop. It also explicitly mentions the micro-turn vs. macro-turn distinction @erights discussed in #84 (comment). See #70 for an earlier attempt, that used the concept of the function execution stack instead of explicitly talking about event loops.
Actually I think the phrasing with " not called before No need for macro/micro event loops etc. The point should be that nothing happens during one user-defined function run, not before it returns control back to the trampolining mechanism (and if there is no active trampolining, it gets deferred to the next tick). The call stack must unwind far enough to have only promise-implementation-specific code on it, but multiple callbacks might be fired in the same event loop turn (and sometimes even in the same as Specifying the " function execution stack to be empty of user code " is imho equivalent, but more complicated… |
I came to say what @briancavalier already said: MutationObserver runs before the next tick of the event loop and after the rest of the code. The "execution stack" phrasing is what we're looking for. It's already in the EcmaScript spec in 10.3 Execution Contexts. It's obscure, but it's always a good time to learn something new, isn't it? |
@bergus and @juandopazo Yeah, I think the challenge is finding a way to define what a "clear execution stack" is in the context of a promise system. E.g. an empty promise system execution stack === ES execution stack containing either or both of: 1) platform scheduler code (e.g. node's own process.nextTick code), 2) promise system code. It seems like there are two routes we could go:
|
A thought: is the clean-stack invariant really what we want? Inspired by @juandopazo on es-discuss, consider a hypothetical resolver API: var p = new Promise(resolve => {
setTimeout(=> {
executeArbitraryUserCode();
resolve.synchronously();
}, 100);
});
p.then(() => console.log("I am executed on a non-clean stack.")); Here This breaks the clean-stack invariant, but I am not sure what practical consequences it has. @erights? |
An attacker could call resolve.sync from the midst of a plan, disrupting clean stack assumptions by the callback |
I don't think I quite understand. What plan and which callback? Example code would be much appreciated :) |
Not tonight. But in any case, what's motivating this? |
Just want to make sure we have the right invariant before we codify it. |
We have to be carefull not to restrict our options for optimisation unnecessarily, and we want to make sure we include the important invariant, so it's esseential we understand what we're trying to encode. |
@domenic good catch! I only skimmed through that part of the ES5 spec. Shouldn't this be enough: "callbacks must be called in a different/future execution context"? |
Alex Russel just commented on es-discuss/public-script-coord:
http://esdiscuss.org/topic/promisefutuasynchronyinthen#content-5 |
We could just appeal to pragmatism and say " We could then leave it to individual implementations and the tests to establish exactly what that means (plus something to clarify in the notes) (P.S. I love that people are now linking to esdiscuss.org rather than pipermail 😄) |
Indeed. That kind of was the intent of the wording in this pull request, as discussed in ab6e22f#readme-md-P5. But I think that was kind of a mistake; we need to figure out exactly which invariants we mean to preserve. Some code samples would be helpful. To get us started, I'll adapt the ones from #100 (comment). We want the sequence of calls to be // (1)
var p = new Promise(resolve => resolve())
a();
p.then(c);
b(); // (2)
var resolveP;
var p = new Promise(resolve => resolveP = resolve);
a();
resolveP();
p.then(c);
b(); // (3)
var resolveP;
var p = new Promise(resolve => resolveP = resolve);
a();
p.then(c);
resolveP();
b(); What else? Note that (3) means our current 1.0 wording (" |
Today (which way is the wind blowing? haha), I'm back to feeling like our current "must not be called before The problem is that it doesn't cover case 3 in @domenic's examples. Coming at this from another angle, is there a way to augment the current language to cover case 3? Seems tricky since this spec doesn't cover the |
Note that end of microtask is an empty stack situation |
Here's a quick attempt at trying to use execution context to extend the current language to cover case 3 above.
I don't think either bullet is quite right, but I do feel like it could be a good direction. Thoughts? |
I think that is exactly what we want. The first bullet point actually is what we already have (" not before then returns "), and the second bullet point should be part of the resolvers-spec (since we here only specify This wording meets all those requirements to have the callbacks in the next micro/macro turn, and it allows trampolining either in native or in (promise) library code. |
@briancavalier Interesting. Your first bullet point takes care of (1) and (2), whereas the second takes care of (3). I tried creating counterexamples using IIFEs to create new execution contexts, but I think it still holds. Here they are, for reference: // (4)
var resolveP;
var p = new Promise(resolve => resolveP = resolve);
a();
(() => p.then(c))();
resolveP();
b(); // (5)
var resolveP;
var p = new Promise(resolve => resolveP = resolve);
a();
p.then(c);
(() => resolveP())();
b(); @bergus I feel somewhat strongly that we can't punt this to the resolvers spec; we are specifying an important aspect of when |
…but only in reference to the
I don't think it would make much sense to specify any chronological/execution-logical relation between the callbacks and that abstract state transition operation - other than "each callback at most once, and not before the transition". There are enough cases where they can happen synchronously together. The only thing we might want to specify how manual Yet, we already have some kind of resolution procedure in this spec here: But what if a thenable is returned? Have a look at
We do know that |
I think I agree with @domenic that we should try to provide some minimal specification of the relationship between state transitions and execution of handlers. One practical reason I feel it's important is that if we don't, then someone can choose to implement Promises/A+, but not the resolvers spec, creating a valid promise implementation that allows case 3. @bergus, a few questions about what you wrote:
Hmmm, not sure I understand what you're saying here, could you clarify what you mean by "synchronously together", and in what context we might want to allow that?
I think I see what you're getting at. Are you saying that by specifying a relationship between state transitions and handler execution, we're being too restrictive for some state transitions which clever implementations might be able to optimize by running handlers within the execution context where the state transition occurs? ("occurs" is a pretty loose term, see below)
I'm not sure what you're getting at, but it seems like we can't do anything to control the order of p.then(f).then(g);
p.then(h); We can't specify an exact order for I think my bullet 2 may not be sufficient because the exact point at which a promise has transitioned is implementation dependent.
For example, I think it leaves open the possibility of doing: function Promise(resolver) {
function resolve(x) {
if(!isPromiseOrThenable(x)) {
_transitionToFulfilled(x);
// This breaks case 3
runHandlers();
} else {
// ...
}
}
function _transitionToFulfilled(x) {
// This is the execution context where the
// promise transitions to fulfilled
}
resolver(resolve, ...);
} In that case, the implementer could make the case that the handlers are being execution outside the execution context where the state transition happens. Seems like it may need a bit more thought. |
Yes. And I don't see what's wrong with that - But let's get to your questions.
Uh, I already feared that this wording is unclear. I meant that they are happening in the same execution context, the handlers getting called immediately after the state flag transitioned. Just like in your example code, where the
Wherever the resolver does not execute anything after the An example for this in user-code could be domenic's wait-implementation from above - it would not make sense to put another tick in between the resolve call and the callbacks here. And there should be a dedicated method like the Other examples will include library-space code, see the next answer.
Exactly. Just think of the very simple
When I even would propose that we can do this whenever a "promise state is adopted" (
As soon as
Yes, maybe we should not specify that at all.
No. I tried to create a case 3, yet not with some hypothetical To rephrase my question: Are the As you noticed yourself, specifying against the execution context (or child thereof??) in which promise transitions [happen] (bullet 2) is not sufficient. Instead, we do need to specify asynchronity against the function which is exposed to the user code. That's the actual point I'm trying to make (sorry for the long reading :-). It is why I think we need to specify that - at least parts of it - in the resolvers spec. It could as well be a rule that is specified here, and only needs to be referenced from the resolvers spec. And it might should/need be referenced from the |
@bergus thanks very much for diving into this with us. Tricky business. I like the idea of re-doing (3) in terms of what we have here. Here's my attempt: // (3')
var resolveP;
var p = aFulfilledPromise.then(() => { then(onFulfilled) { resolveP = onFulfilled; } });
a();
p.then(c);
resolveP();
b(); We still definitely want a-b-c for this. I think the more complicated questions you were asking are getting into other territory that shouldn't necessarily be specified, as @briancavalier said. I also agree with your examples (e.g. Your examples actually sway me back toward something very like what we have in this pull request, because the pull request contains the idea of "whenever you want, in whatever combination you want, as long as it's after the turn in which Yet, dealing with the subtleties of the "end phase" of the event loop (in which e.g. mutation observers and Node.js >= 0.10 What about this??
I think this works! It's saying that by the time you call Thoughts!?? |
Just trying to prove that what we currently have is sufficient, to keep the desired simplicity :-)
That doesn't work because the
Yeah, and that is too strict. It doesn't account for brians example
I do not understand why you include the parent execution contexts? It sounds to me like the stack reset which you are talking of prevents trampolining in library code (as in brians example), maybe I did misunderstood this? I still think if some of the parent execution contexts have access to a resolver which they intend to call and nevertheless attach a handler via |
My reading of this is the same as @bergus: the "or in any parent contexts" bit prevents clever trampolines. If we remove that bit, it works: "onFulfilled and onRejected must not be called in the execution context of the corresponding call to Which is mostly the same as our current "not before then returns" language. This part does seem sufficient to me. I'm still not sure about the case 3, tho. |
Disagree! The trampoline parent context would not contain the call to |
But then it's neither a parent, nor what I would call a trampoline?
Does it? That would not be capable of trampolining |
Hmmm, I need to think more closely about your wording.
Yes, I agree this is important. I feel we must allow
Should also allow |
Argh, you got me.
Yeah. Example: p1.then(function a() { p1.then(c); b(); }); We want the execution order to be
As we can see, Damn! |
I'll have time to comment on Monday. |
Yeah @domenic I totally forgot to post them :-) The "platform context" seems to forbid synchronous resolving as in your example above - technically there is user code on the execution stack. It would disallow exposing platform code for explicit requests to resolve a promise immediately. Gonna try a proposal myself now (still basically what we currently have, but clarifying some aspects):
|
Why do you want to permit that? It would enable plan interference attacks. |
There are examples above.
I don't really understand what you mean, and have never seen that term. Could you please link to a reference, or elaborate? I've found your post on esdiscuss, but I don't see how that applies here. Can you make an example of how a promise implementation conforming to the proposal would support malicious code? |
http://erights.org/talks/promises/paper/tgc05.pdf http://www.infoq.com/presentations/Secure-Mashups-in-ECMAScript-5 with slides at |
@erights: Right, I've already read them once but forgot that term. And I should've use GoogleScholar to search :-/ However, I don't see how my proposal would change anything in regards to that attack vector. What exactly are you referring to? Nested publication bugs cannot happen due the nature of promises - they are only resolved once. Nested subscription (with out-of-order propagation) is prohibited by point 6 of the spec, and "aborting the wrong plan" is not dealed with explicity, yet implied by " must be called". |
Let's take nested publication. Under your proposal, the callback might still happen after the .then returns but before the for-loop is finished. |
@erights I also am having trouble seeing the problem.
By "for-loop" here, I'm assuming you mean the It seems like this is a problem only if the callback is allowed to run out of order, or is allowed to abort the @bergus' latest wording, which is a clarification on our current "not before then returns" and my first attempt, seems pretty good to me. The note mentioned micro and macro tasks seems clear. Some folks may scratch their heads if they haven't heard those terms before, but we can certainly link to some useful information to help them understand. |
The for-loop thing is implementation-specific, and whether you are preventing bugs by wrapping in try-catch and queuing (which you have to do anyway) or by scheduling each single callback on a new event loop turn should be left open. |
Perhaps I am misunderstanding. I am fine with a trampoline-based implementation if there are no user stack frames below it. Does this wording somehow imply this? The for-loop I'm referring to is in the example code being attacked, not in the implementation. |
Yeah, but I think the spec should be open enough to allow even that. Synchronous resolve calls won't be the standard, but they can be useful in some situations (I've called them "tail resolution" above). A conforming library won't need, but should be allowed to offer such functionality.
OK, it seems we misunderstood you then - sorry. Could you post that example code, please, to ensure that we talk about the same things? |
To clarify the case for "synchronous resolve calls": if you are already at least one turn past the original call to const fs = require("fs");
// This is part of a library packaged with the promise implementation,
// so it has access to internal underscored properties. As such, it
// bypasses the usual way of creating and manipulating promises in
// favor of assumedly-faster internal state manipulation.
export function promisifiedReadFile(fileName) {
const promise = new Promise((resolve, reject) => {});
fs.readFile(fileName, (err, data) => {
if (err) {
promise._reason = err;
promise._state = "rejected";
} else {
promise._value = data;
promise._state = "fulfilled";
}
// This *synchronously* fires any `onFulfilled` or `onRejected` callbacks
// that have been added via `promise.then`. Since we know that file I/O
// always takes at least one tick, we know that we are past the tick in which
// `promise.then` was called. But, we're not in a clean-stack situation.
promise._fireSettledCallbacks();
});
return promise;
} // This is an example of a promise implementation which allows any consumer to violate
// the clean stack invariant, but does *not* allow generic synchronous resolution.
// It does this by tracking ticks, ensuring that `onFulfilled` and `onRejected` are never
// called in the same tick as `then`.
//
// Apologies for all the missing details, but hopefully you see the idea...
export function Promise(resolver) {
try {
resolver(resolve, reject);
} catch (e) {
reject(e);
}
function reject(r) {
this._rejectionHandlers.forEach(function (onRejected) {
if (onRejected._isNextTick) {
// synchronously call and deal with `onRejected`
} else {
// do it asynchronously
}
});
}
this._rejectionHandlers = [];
this.then = function (onFulfilled, onRejected) {
onRejected._isNextTick = false;
process.nextTick(() => onRejected._isNextTick = true);
this._rejectionHandlers.push(onRejected);
// ...
};
} |
@erights, would love your comments on the "synchronous resolve calls" use case, or some example code that helps us all understand the plan interference attack idea as it applies to this question. |
I am becoming increasingly convinced that accommodating the cases I give above are important for callback-performance parity, since they avoid doing an "extra" nextTick if you're already doing something asychronous. |
Hmmm, so you're saying that an implementation could essentially provide some API (e.g. the setting of Similarly, in some discussions about IndexDB compatibility in when/issues#148, it was suggested that a library could expose a That said, tho, doesn't our current wording "not before then returns" allow your example? I.e. since it runs the handlers synchronously inside the resolver's public API and not inside |
It could go further than that. It could be that
Yes indeed. Our current wording does allow this, but it also allows the not-so-desirable case 3. In practice, here's the difference: /// (6)
function example6() {
return new Promise(function (resolve) {
console.log("6A");
resolve();
console.log("6B");
});
}
example6().then(function onFulfilled() {
console.log("6C");
}); This must give 6A, then 6B, then 6C. But, in contrast: (7)
function example7() {
return new Promise(function (resolve) {
setImmediate(function () {
console.log("7A");
resolve();
console.log("7B");
});
});
}
example7().then(function onFulfilled() {
console.log("7C");
}); This could give 7A, 7B, 7C or it could give 7A, 7C, 7B. That is, |
That is interesting. So the assumption here is that if you are using promises to represent operation that will complete asynchronously, then Def seems worth thinking through this ... a few questions spring to mind immediately:
|
Yes, that's what DOM Promise used to do with an optional |
Sorry for taking a break on this. I think it's the main thing holding us up on the 1.1 release.
Those of course need to be nextTicked. The logic inside
Right, hrm. We need to go back to that original test case you wrote: kriskowal/q@3f32632 var fulfilled = adapter.fulfilled();
function when(x) {
return fulfilled.then(() => x);
}
var i = 0;
var p = when({
then(cb) {
cb(i);
// vs
// setTimeout(() => cb(i));
}
});
i++;
p.then(console.log); (The desire is for this to always log I believe this is unaffected. Following my above internal heuristics for how |
I would like to close this, this weekend. I think the correct step here is to strengthen our requirement very slightly from the current one, to something like @bergus's. It is still stronger than our current one, but weaker than the complete clean-stack requirement. If we ever get a more concrete reason why we need the completely-empty-stack requirement, we'll release another spec revision with that strengthening. I will create a new pull request tonight-ish. |
@bergus's wording:
does not work: // (3'')
var resolveP;
function x() {
var p = aFulfilledPromise.then(() => { then(onFulfilled) { resolveP = onFulfilled; } });
a();
p.then(c);
}
x();
resolveP();
b(); An implementation could follow that wording and still do a-c-b. The execution context tree for such an implementation would be as follows:
That is, "the call to Argh. |
Note also that @briancavalier's idea:
prohibits the synchronous-resolve-if-already-async optimization I want to preserve. So that's out. |
My latest try was:
This seems to prohibit the following: (7)
function example7() {
return new Promise(function (resolve) {
setImmediate(function () {
console.log("7A");
function x() {
resolve(); // oops, execution context stack contains `x`
}
x();
console.log("7B");
});
});
}
example7().then(function onFulfilled() {
console.log("7C");
}); which I would like to work. |
And I believe that, even if make "end" of event loop turn clearer, my original phrasing in this pull request
breaks trampolining, as can be seen from this comment. Getting discouraged here. Have to take a step back. |
The only thing I can think of is along the lines: "Either one event loop turn must have passed, or only platform code must be in the execution context stack." |
Starting over with a new statement of the problem in #139. |
This fixes #100, by explicitly calling out the concept of an event loop. It also explicitly mentions the micro-turn vs. macro-turn distinction @erights discussed in #84 (comment).
See #70 for an earlier attempt, that used the concept of the function execution stack instead of explicitly talking about event loops.
I am not 100% sure this is the way to go, but it seems like the path of least resistance. I'd love if people threw out alternatives. Maybe the best I have is
with [1] explaining what "user code" means, e.g. promise implementation code and [native code] or Node.js platform code is not user code, but all other things are.