-
Notifications
You must be signed in to change notification settings - Fork 72
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
Handle symbols installed on Promise by Node's async_hooks
#1115
Conversation
958be31
to
4dbf59d
Compare
packages/init/node-async_hooks.js
Outdated
}; | ||
|
||
const setAsyncIdFallback = (promise, symbol, value) => { | ||
const state = getAsyncHookFallbackState(promise, true); |
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.
(nitpick) 3 function signatures here have boolean arguments, each meaning different things. using an options object where the boolean is named in the caller as well would limit the lookups one has to make while reading the body of the current function to remember what the boolean did.
packages/init/node-async_hooks.js
Outdated
bootstrapData.length = 0; | ||
const { length } = promisesData; | ||
if (length > 1) { | ||
// console.warn('Found multiple potential candidates'); |
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.
Are you aware of a situation when this would happen? AFAIR it shouldn't be possible.
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 did see it happen at one point when I was exploring, and then couldn't reproduce. not sure why exactly, but I kept the check around.
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 mean the init hook was called twice. I'd consider that a bug in Node.
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 it created multiple promise instances for some reason.
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.
LGTM modulo some nits
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 am uncomfortable making these weird symbols into apparent well-known symbols by adding them as statics to the Symbol
constructor. Let's discuss before we go forward with this.
packages/init/node-async_hooks.js
Outdated
if (!wellKnownName) { | ||
throw new Error('Unknown symbol'); | ||
} else if (!Symbol[wellKnownName]) { | ||
Symbol[wellKnownName] = symbol; |
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 am uncomfortable making these weird symbols into apparent well-known symbols by adding them as statics to the Symbol
constructor. Let's discuss before we go forward with this.
9d2b638
to
6f6fffb
Compare
7764ea1
to
d62e8cf
Compare
@erights I switched away from making the async hooks symbols registered and using the new ability to name unique symbols in the whitelist. PTAL |
d62e8cf
to
02d90b9
Compare
It was brought up that export class AsyncResource {};
export const createHook = () => ({ enable() {}, disable() {} }); |
899a957
to
5139dad
Compare
02d90b9
to
65c52e3
Compare
I've updated the PR to only include the async_hooks patch on node based on conditional exports, and included a test, but I'm getting an apparently unrelated failure:
attn @kriskowal |
@mhofman I’ve added a commit that should unblock the build with |
packages/ses/package.json
Outdated
@@ -35,11 +35,11 @@ | |||
"types": "./index.d.ts", | |||
"exports": { | |||
".": { | |||
"import": "./dist/ses.umd.js", |
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.
Should there be a "browser"
fallback keeping UMD for the browser use case (after import and require) ?
Well except I've taken a reliance on self references. I think I'll move the async hooks logic in a separate taming package that endo/init takes a dependency on for now? |
Co-authored-by: Richard Gibson <richard.gibson@gmail.com>
1b224d9
to
4b73064
Compare
@erights PTAL |
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 just noticed that all the normal source files of this init
package are at top level in the package, rather than nested in a src
directory. Please move the normal source files into a src
directory.
Other than that, my review is still in progress, but looking good to me so far.
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.
Review done. Everything I understood LGTM except the directory structure. I did look at all the node/async_hook manipulation code, and it seemed fine to the extent I understood it. Mostly I was amazed at how many cases you had to wrestle with!
I believe the files were in the root because they are all entrypoints. While |
Yes, @michaelfig and I have agreed that, due to limitations of interoperability between Node.js versions that recognize For the same reason, we determined that the |
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 approve in terms of general structure and willingness to maintain. I don’t begin to grasp the implications of async_hooks
, but the effect of this change seems good to me.
Closes #1105
Layers on top of #1114
Node 14.18 and 16.61 introduced a change (nodejs/node#39135) to
async_hooks
that enabled the fast-path for promise hooks even in the presence of adestroy
hook. The fast-path logic removes the wrapper object which was previously used to hold the async hooks metadata for each promise instance, such as async ids and a property bag related to the liveness of the promise instance, causing these to be attached directly to the promise instance as own symbols.When a debugger enables async stack traces (
Debugger.setAsyncCallStackDepth({maxDepth})
withmaxDepth
> 0), node's inspector support internally enablesasync_hooks
with adestroy
hook 2 3 4, causing all promises to gain these symbols.The
init
promise hooks is the main source of symbol attachments. It runs synchronously right after the constructor returns and before the caller gets the reference. While that would normally cause all promise instances to gain these symbols as own property before the program gets a chance to freeze them, other promise hooks enforce the presence of async ids on existing promise objects. Ifasync_hooks
is enabled after a promise has already been created and frozen, for example by attaching the debugger later, it will fail to attach these symbols, throwing aTypeError
synchronously.Until node removes these symbols as own property, or handles assignment failure (see nodejs/node#42229), we workaround the issue by installing accessors for these symbols on the
Promise.prototype
, which defines them as an own property (non-enumerable to avoid polluting the console output), and falls-back to aWeakMap
for non-extensible promise instances.The accessors handle both async ids, and the
destroyed
symbol. While the latter is not strictly necessary as it's always installed duringinit
and never late, it's more consistent to handle it similarly to async ids, and has the added benefit of removing enumerable pollution.The
async_hooks
logic is added to@endo/init
because of the dependency on a node import.lockdown
is updated to whitelist these symbols on the promise prototype. It would be better to havelockdown
remove these by default and require a taming option to leave them in place, but I didn't find a way to thread that into the whitelist logic to make it conditional.Footnotes
https://github.com/nodejs/node/issues/42229#issuecomment-1062145546 ↩
https://github.com/nodejs/node/blob/d4d0b09122d91478881ee7fd36eb3c9f78f8452d/src/inspector_agent.cc#L411-L424 ↩
https://github.com/nodejs/node/blob/d4d0b09122d91478881ee7fd36eb3c9f78f8452d/lib/internal/bootstrap/pre_execution.js#L303-L316 ↩
https://github.com/nodejs/node/blob/master/lib/internal/inspector_async_hook.js ↩