-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
lib: make sure globals can be loaded after globalThis is sealed #46831
base: main
Are you sure you want to change the base?
Conversation
Since we made some of the globals lazily loaded to reduce startup overhead, and tried to update then as data properties once loaded, the lazily-loaded globals can no longer be loaded once globalThis is freezed. Fix this by not attempting to overwrite the property on loading these globals (so the property descriptors won't be spec-compliant in this case, but at least it's a good compromise between breakages and startup overhead). Note that certain globals that come from undici are still broken because undici attempts to define a global dispatcher symbol on globalThis. This is reflected in the test.
lib/internal/util.js
Outdated
@@ -532,7 +533,10 @@ function defineLazyProperties(target, id, keys, enumerable = true) { | |||
mod ??= require(id); |
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 I suggest moving this line inside the if
statement below? If the getter can't be replaced, it should do as little work as possible in the common path.
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 suppose by the if
statement you meant if (lazyLoadedValue === undefined)
? In that case it made sense to me, otherwise (if (!ObjectIsFrozen(this))
) that's probably not the desired behavior (we can still return the result in the getter, just can't fix up the descriptor when it's frozen).
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.
Consider updating the commit message. I left a few suggestions which would I think simplify the tests.
try { | ||
success.set(key, globalThis[key]); | ||
} catch (e) { | ||
failures.set(key, e); | ||
} |
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.
try { | |
success.set(key, globalThis[key]); | |
} catch (e) { | |
failures.set(key, e); | |
} | |
const access = () => globalThis[key]; | |
if (knownFailures.has(key)) { | |
assert.throws(access, { name: 'TypeError' }); | |
failures.delete(key); | |
} else { | |
access(); | |
} |
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 is it deleting the failure (which is undeclared if we apply the suggestion above)?
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.
Sorry I meant
try { | |
success.set(key, globalThis[key]); | |
} catch (e) { | |
failures.set(key, e); | |
} | |
const access = () => globalThis[key]; | |
if (knownFailures.has(key)) { | |
assert.throws(access, { name: 'TypeError' }); | |
knownFailures.delete(key); | |
} else { | |
access(); | |
} |
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 this version would lead to less debuggable messages - for example if the error is caused by redefining something else on a global in the implementation of another (e.g. the undici symbol for the known failures listed here), the error message would not show which key is causing the trouble, and only shows the symbol which is an implementation detail of that key. So fixing the known failures takes 4 trial-and-errors. Whereas having a failures map for them all and logging the map only takes 1 trail-and-error.
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.
What do you think of the following then?
try { | |
success.set(key, globalThis[key]); | |
} catch (e) { | |
failures.set(key, e); | |
} | |
const access = () => globalThis[key]; | |
if (knownFailures.has(key)) { | |
assert.throws(access, { name: 'TypeError' }); | |
knownFailures.delete(key); | |
} else { | |
try { access(); } catch { unexpectedFailures.add(key); } | |
} |
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.
(And also I personally prefer to avoid one-line helpers like access()
here in the test because they make the test failures harder to read. IMO tests are usually easier to understand if we can just be verbose and write the code being tested directly even if it involves some repetition - as long as it's not too much repetition)
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.
At least you can agree that the success
map is not useful, nor for the test itself, and likely not for debugging either?
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.
What do you think of the following then?
I think this is more complicated than the original version, which just adds results on success and error on failure to two maps instead of trying to add or delete anything? At least it'd take me a while to understand what the second version is doing, whereas the original one doesn't take much brain power to understand.
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.
At least you can agree that the success map is not useful, nor for the test itself, and likely not for debugging either?
That avoids having to add a comment to skip the no-unused-expressions eslint check. I figure if we have to throw extra code to avoid it, might as well just put them into a map, in case one wants to find out what can actually be loaded (for example, to find out fetch
can be loaded, but other fetch-related classes can't).
const assert = require('assert'); | ||
|
||
Object.freeze(globalThis); | ||
const keys = Reflect.ownKeys(globalThis).filter((k) => typeof k === 'string'); |
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.
const keys = Reflect.ownKeys(globalThis).filter((k) => typeof k === 'string'); | |
const keys = Reflect.ownKeys(globalThis); |
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.
Hmm, I don't think having only the keys is an improvement for debugging the failures? At least it helped my figuring out what's going on when I could just console.log()
the failures. Sometimes I think there is a tendency in our tests that made the tests too simple and DRY so it makes the tests harder to debug, but I'd personally prefer debuggability over simplicity...
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.
Did you post your comment on the right thread? This suggestion is removing a filter (so we run the test on all the keys), it's not adding 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.
Oh, right, the comment should be posted to the for-loop below. But I don't think we need to run the test on all keys though, otherwise we run into the undici dispatch symbol, and I don't see that it's Node.js core that should test the symbol works on a frozen globalThis?
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.
Restricting to string keys is a bit weird, I think it would be better to be able to access all global keys when the global object is sealed, and add to one that would fail to the expected failures set. If that's harder than it's worth, we should add a comment explaining 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.
Personally I'd only care about string keys because it would be weird that contextual lookup on the global stops working once globalThis is sealed. Symbols not so much because one can't do contextual lookup on the global with symbols, and also these symbols aren't necessarily visible to/fixable by Node.js core (like the undici one). But I can add a comment.
const failures = new Map(); | ||
const success = new Map(); |
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.
const failures = new Map(); | |
const success = new Map(); |
} | ||
} | ||
|
||
assert.deepStrictEqual(new Set(failures.keys()), knownFailures); |
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.
assert.deepStrictEqual(new Set(failures.keys()), knownFailures); | |
assert.deepStrictEqual(knownFailures, new Set()); |
…nodejs#46831 Co-authored-by: Antoine du Hamel <[email protected]>
Since we made some of the globals lazily loaded to reduce startup overhead, and tried to update then as data properties once loaded, the lazily-loaded globals can no longer be loaded once globalThis is freezed. Fix this by not attempting to overwrite the property on loading these globals (so the property descriptors won't be spec-compliant in this case, but at least it's a good compromise between breakages and startup overhead).
Note that certain globals that come from undici are still broken because undici attempts to define a global dispatcher symbol on globalThis. This is reflected in the test.
Refs: #46788