-
Notifications
You must be signed in to change notification settings - Fork 312
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
Improve handling of extend lifetime promises #1049
Conversation
@jakearchibald, @annevk, this is about (1) introducing reference count for checking extend lifetime promises and (2) allowing @domenic, it'd be great if you had time to review these particular steps where I referenced ECMAScript internal slots:
/cc @wanderview |
Note that I didn't remove the extend lifetime promises definition. I presume we might want to check if we can rely solely on the pending promises count definition replacing the extend lifetime promises. But I'm not entirely sure yet. The extend lifetime promises is being referenced by other specs to run their own custom callbacks (e.g. https://wicg.github.io/BackgroundSync/spec/#fire-a-sync-event). Examining the extend lifetime promises also allows checking whether the extendable event succeeded or failed depending on the contexts. |
(1) This changes the approach to unsetting the extendable events' extensions allowed flag. This change introduced a reference count based approach instead of the promise-copying-and-checking approach. (2) This also extends the opportunities of the lifetime extension by allowing calling waitUntil() within microtasks queued by the given promise's Promise.prototype.then callback. Related issues: - #931 (1) - #935 (2) - #1039 (2)
@@ -1289,7 +1289,9 @@ spec: webappsec-referrer-policy; urlPrefix: https://w3c.github.io/webappsec-refe | |||
|
|||
An {{ExtendableEvent}} object has an associated <dfn for="ExtendableEvent">extend lifetime promises</dfn> (an array of <a>promises</a>). It is initially an empty array. | |||
|
|||
An {{ExtendableEvent}} object has an associated <dfn id="extensions-allowed-flag">extensions allowed flag</dfn>. It is initially set. | |||
An {{ExtendableEvent}} object has an associated <dfn for="ExtendableEvent" id="extensions-allowed-flag">extensions allowed flag</dfn>. It is initially set. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to invert this somehow. Flags and booleans that default to false are much more intuitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Will try to change it to something like extensions opportunity closed flag instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag is replaced by the use of the pending promises count and the dispatch flag.
|
||
Note: If no lifetime extension promise has been added up to this point (i.e., at the end of the task that called the event handlers), the [=ExtendableEvent/extensions allowed flag=] is immediately unset. Calling {{ExtendableEvent/waitUntil()}} in subsequent asynchronous tasks will throw. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need both the flag and the count then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the flag is still needed. Otherwise, we can't determine whether the pending count is in the initial state zero or zero as the extension possibility is over.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you just check the event dispatch flag or some such for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was between leaving the flag as a variable helping the understanding of the control flows and removing it as the cleanup of unnecessary variables. I think the latter wins, so removed it.
:: None | ||
|
||
1. Wait until |promise| is settled [=in parallel=]. | ||
2. [=Queue a microtask=] to run the following substeps: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You cannot queue microtasks from something that runs in parallel. Needs to be a regular task.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, why is that? Microtasks are just a type of task, in general, so I thought this would work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Microtasks belong to a task. But there's no task here they could belong too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be queued from the task where the promise is settled. I explicitly used promise’s relevant settings object's responsible event loop to queue this microtask.
1. Wait until |promise| is settled [=in parallel=]. | ||
2. [=Queue a microtask=] to run the following substeps: | ||
1. Decrease |event|'s [=ExtendableEvent/pending promises count=] by one. | ||
1. For each |reaction| in |promise|.\[[PromiseFulfillReactions]]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems extremely hacky and probably wrong. I don't think we should be poking at the guts of promises.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't reviewed in detail yet, but I agree poking at [[PromiseFulfillReactions]] is a bad idea. Poking at [[PromiseState]] and [[PromiseResult]] is OK though IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed the comments in #1039 (comment).
This doesn't appear to be a good idea. I'm not sure what is trying to be accomplished, but as specced this means that given const p = getSomePromise();
const q = p.then(doSomething);
e.waitUntil(p);
const r = p.then(doSomethingElse); will only wait on |
@domenic: that's exactly the designed behavior. You can extend the lifetime again by calling |
My point is that it's a bizarre design that the lifetime waits on q when you didn't pass q to waitUntil. |
@domenic, let me try to clarify the logic first (and then let's continue about the design):
I think it also will wait on r too. My intention was that when Handle Extend Lifetime Promise called in waitUntil goes async, Referencing Promise.[[PromiseFulfillReactions]] from the internal spec algorithm seems hacky but I think the implementation also has to attach some kind of native handlers to catch and handle the then callbacks. Still looking for a better way to spec it but haven't found one yet. |
FYI. The second code example in #935 (comment) is the original problem statement for this design. |
No implementations provide such hooks today, and they're not meant to: doing so is invasive and constrains the promise implementation from performing optimizations that we're actively working on. Internal state is meant to stay internal. If you can't accomplish this with the public .then API, I'd strongly suggest rethinking the design. Besides, it's just strange that it's waiting on promises you didn't pass to waitUntil. |
I'll take this design discussion back to #1039. I thought @wanderview commented there that Firefox already implemented this behavior. Would like to look into that and also discuss with Chromium implementers around this too. |
This makes waitUntil() wait only the given promises instead of all the promises in the given promises' chains. So, it removes the direct references to the promises internal slots. This replaces the extensions allowed flag with the combinations of the pending promises count and the state of the event's dispatch flag.
3210c4d
to
93b6499
Compare
1. Unset |event|'s <a>extensions allowed flag</a>. | ||
|
||
The user agent *should not* <a lt="terminate service worker">terminate</a> the [=/service worker=] associated with |event|'s <a>relevant settings object</a>'s [=environment settings object/global object=] until |event|'s <a>extensions allowed flag</a> is unset. However, the user agent *may* impose a time limit to this lifetime extension. | ||
1. Wait, [=in parallel=], until |promise| is settled and |promise|'s <code>then</code> methods, if any, in the task where |promise| has been settled have executed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would you test the "and" requirement here? That should be removed I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without the condition after "and", I'm concerned about the race between this microtask (decrease the count) and the promise.then methods that can be called in the main thread (event handler tasks.) Is it an unnecessary concern?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I said, this cannot be a microtask. It can only be a task. And therefore it should be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, if I queue a normal task here. It'd be all fine. But I thought it'd be ideal if we could use a microtask in a way. Firefox actually queues a microtask to check this: https://dxr.mozilla.org/mozilla-central/source/dom/workers/ServiceWorkerPrivate.cpp#379.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That code doesn't seem to communicate across threads though.
|
||
The user agent *should not* <a lt="terminate service worker">terminate</a> the [=/service worker=] associated with |event|'s <a>relevant settings object</a>'s [=environment settings object/global object=] until |event|'s <a>extensions allowed flag</a> is unset. However, the user agent *may* impose a time limit to this lifetime extension. | ||
1. Wait, [=in parallel=], until |promise| is settled and |promise|'s <code>then</code> methods, if any, in the task where |promise| has been settled have executed. | ||
2. [=Queue a microtask=], on |promise|'s [=relevant settings object=]'s [=responsible event loop=], to decrease |event|'s [=ExtendableEvent/pending promises count=] by one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I said earlier, you cannot queue microtasks from parallel algorithms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well.. I couldn't find any constraints a microtask should belong to a particular task. Doesn't it work even if the step designate the target event loop? If this doesn't make sense, should I queue a task to queue this microtask?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now ECMAScript's "PromiseJobs" are queued through https://html.spec.whatwg.org/#enqueuejob(queuename,-job,-arguments). I think those steps are not run in the main thread but in a thread in the JS engine internals.
you cannot queue microtasks from parallel algorithms.
The reason it doesn't make sense is because it is run in a parallel thread? Or it's running off the main thread?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just queue a task to decrease the thing. It doesn't have to be a microtask.
Queue a microtask from a parallel thread is racy as you say, since you don't know what task it ends up in. User agents only have the ability to queue tasks to get things into the main thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed it to using a normal task.
@annevk, do you have any further comments? I'd like to merge it if it's ok. |
I guess a remaining problem is that you haven't defined the task source. |
Hmm, do you even need to go in parallel/queue a task? It seems like for promises you should be able to just do "upon fulfillment"/"upon rejection". |
That's a good point. If you don't queue a task though, it does start to matter when the fulfillment observer is attached, right? I guess as long as that's well-defined it's not really an issue, but would be worth testing. Do we have tests for this feature? |
Not sure if its exactly what you mean, but we wrote this wpt test: |
I don't think we can guarantee the expected sequence by using "upon fulfillment"/"upon rejection" here. This |
Why does it need to happen after the then callbacks? That doesn't show up in the problem description. If you want it to happen after |
In the following snippet for example: addEventListener('push', e => {
let outResolve;
let p = new Promise(resolve => { outResolve = resolve; });
e.waitUntil(p); // Upon fulfillment/rejection of p inside
p.then(() => {
e.waitUntil(q);
});
setTimeout(() => outResolve(), 1);
}); I think the "Upon fulfillment/rejection of p" triggered inside of |
Hmm I guess that does appear in #1039 (comment). I think that's a bad design, but it's not a huge deal. I guess if you want that you can just do "upon fulfillment/rejection, queue a task". |
@jungkees yeah, I see. I think that is a bad design and if you want that code to work it should be coded as addEventListener('push', e => {
let outResolve;
let p = new Promise(resolve => { outResolve = resolve; });
e.waitUntil(p.then(() => {
e.waitUntil(q);
});
setTimeout(() => outResolve(), 1);
}); since then you actually "wait until" the argument (instead of "wait until-but-also-wait-an-extra-turn" the argument). But it's not a huge deal. |
Yeah, "wait until-but-also-wait-an-extra-turn" the argument is the original problem statement and the behavior people agreed on. With your comment, I sort of come to think of again whether this was a right decision but would like to settle on what we decided. For the spec wording, which of the following would be better?
or
or
|
Yep, that's fair; I totally understand.
IMO |
OK. That sounds good. Thanks! |
@domenic: thanks for the clarification. I agree it's a bit bizarre to have a derived promise extend the lifetime and that only the argument to |
(1) This changes the approach to unsetting the extendable events'
extensions allowed flag. This change introduced a reference count based
approach instead of the promise-copying-and-checking approach.
(2) This also extends the opportunities of the lifetime extension by
allowing calling waitUntil() within microtasks queued by the given
promise's Promise.prototype.then callbacks.
Related issues: