Skip to content
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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

joyeecheung
Copy link
Member

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

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.
@joyeecheung joyeecheung marked this pull request as ready for review February 25, 2023 00:12
@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. util Issues and PRs related to the built-in util module. labels Feb 25, 2023
@@ -532,7 +533,10 @@ function defineLazyProperties(target, id, keys, enumerable = true) {
mod ??= require(id);
Copy link
Member

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.

Copy link
Member Author

@joyeecheung joyeecheung Feb 27, 2023

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).

lib/internal/util.js Outdated Show resolved Hide resolved
@nodejs-github-bot
Copy link
Collaborator

@aduh95 aduh95 changed the title lib: make sure globals can be loaded after globalThis is freezed lib: make sure globals can be loaded after globalThis is sealed Feb 27, 2023
Copy link
Contributor

@aduh95 aduh95 left a 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.

Comment on lines +16 to +20
try {
success.set(key, globalThis[key]);
} catch (e) {
failures.set(key, e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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();
}

Copy link
Member Author

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)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I meant

Suggested change
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();
}

Copy link
Member Author

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.

Copy link
Contributor

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?

Suggested change
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); }
}

Copy link
Member Author

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)

Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member Author

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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const keys = Reflect.ownKeys(globalThis).filter((k) => typeof k === 'string');
const keys = Reflect.ownKeys(globalThis);

Copy link
Member Author

@joyeecheung joyeecheung Feb 28, 2023

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...

Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member Author

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.

Comment on lines +13 to +14
const failures = new Map();
const success = new Map();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const failures = new Map();
const success = new Map();

}
}

assert.deepStrictEqual(new Set(failures.keys()), knownFailures);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert.deepStrictEqual(new Set(failures.keys()), knownFailures);
assert.deepStrictEqual(knownFailures, new Set());

test/parallel/test-frozen-global.js Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-ci PRs that need a full CI run. util Issues and PRs related to the built-in util module.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants