-
Notifications
You must be signed in to change notification settings - Fork 30.1k
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
VM ESM with dynamic imports resolves promises before linking is complete #33216
Comments
cc @devsnek |
The example given looks sufficiently contrived/broken (multiple linker functions, multiple calls to evaluate, etc) that I am not convinced there is a bug with node at this point. If a simpler reproduction can be provided I will gladly take a look. |
That doesn't bode well for my current implementation in Jest then as I have pretty much the same approach there 😅 I can remove the |
@SimenB here's an example loader that might help: 'use strict';
const { SourceTextModule } = require('vm');
const path = require('path');
const fs = require('fs');
const cache = new Map();
function load(specifier, referrer) {
const filename = path.resolve(path.dirname(referrer.identifier), specifier);
if (cache.has(filename)) {
return cache.get(filename);
}
const source = fs.readFileSync(filename, 'utf8');
const m = new SourceTextModule(source, {
identifier: filename,
importModuleDynamically: entry,
});
cache.set(filename, m);
return m;
}
async function entry(specifier, referrer) {
const m = load(specifier, referrer);
if (m.status === 'unlinked') {
await m.link(load);
}
if (m.status === 'linked') {
await m.evaluate();
}
return m;
}
entry('./a.mjs', { identifier: process.cwd() + '/[node]' }); While you can evaluate and link each layer of the graph manually, it is much simpler to just call |
Thanks @devsnek, that's cleaner indeed! And it does fix the error in my original reproduction as well. However, making the same change in Jest doesn't fix the issue there. I've now spent some more time on it, and I'm now able to see the same error I can see in Jest - which is that it seems like the linking tries to link modules from different contexts, or something? I've changed the repo in the OP to have a fake super-dumb test framework and added a "teardown" when a test complete (setting Example of the error. $ node --experimental-vm-modules index.js
(node:9917) ExperimentalWarning: VM Modules is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Map(1) { 'goodbye' => 'success!' }
Trying to load /Users/simen/repos/jest-esm-dynamic-import-error/lib/hello.js from test ./__tests__/goodbye.test.js
Map(1) {
'hello' => Error: test has already completed
at TestRuntime.load (/Users/simen/repos/jest-esm-dynamic-import-error/index.js:46:19)
at async TestRuntime.entry (/Users/simen/repos/jest-esm-dynamic-import-error/index.js:22:19)
at async importModuleDynamicallyWrapper (internal/vm/module.js:422:15)
at async echo (/Users/simen/repos/jest-esm-dynamic-import-error/lib/index.js:2:29)
at async /Users/simen/repos/jest-esm-dynamic-import-error/__tests__/hello.test.js:4:18
at async TestRuntime.globalThis.runTests [as framework] (/Users/simen/repos/jest-esm-dynamic-import-error/testFramework.js:12:13)
at async TestRuntime.runTest (/Users/simen/repos/jest-esm-dynamic-import-error/index.js:71:25)
at async runTest (/Users/simen/repos/jest-esm-dynamic-import-error/index.js:83:21)
at async Promise.all (index 0)
}
test run complete! I'm really not sure what the bug is, but there are 3 things wrong with the behavior that I can see
EDIT: Just pushed another commit which removed the timeout stuff I added, and the test now fails almost every time I run it |
@SimenB you have a TOCTOU here because of the async file op: https://github.com/SimenB/node-vm-esm-promise-awaiting/blob/d7e5baf9927e5400432575ae8de55253c24a7e78/index.js#L39-L57 |
Is this still an issue? |
Yes, but I believe #36351 might be the underlying reason. Not 100% sure - would be interesting to attempt to reproduce this after that issue is fixed. I haven't found the time to dig into this yet, but I'm hopeful I'll find the time in the not too distant future (I assume a bunch of modules will start to be ESM only once Node 10 is EOL, at which point good support in Jest will be very much needed) |
I updated the repo now so it still works on newer versions of Node (the TLA PR removed the result from @devsnek I don't quite understand why the TOCTOU concern you mention is an issue. I tried adding a super simple mutex, but that is never triggered (i.e. the same module is not loaded more than once in the same "test run"). You can remove the module cache (or throw on module cache hits) and we still get the same error. One "test" never accesses the same module twice, so the cache shouldn't matter. Both tests access the same module, but they should be independent since they're evaluated in different contexts. I agree removing the async It looks to me like there's some sort of unwanted cached module somewhere, so the wrong thing runs, do you think it might be the same issue as #36351? |
@SimenB the issue is this:
|
Right, but in this case 1. never hits - the same module is never loaded twice (from the same test). So I don't think that's an issue in practice? But I see you're saying step 2 is the issue, so maybe the cache part doesn't affect things. But does that mean we cannot run |
What steps will reproduce the bug?
See https://github.com/SimenB/node-vm-esm-promise-awaiting
Run
node --experimental-vm-modules index.js
to reproduce.How often does it reproduce? Is there a required condition?
Almost every time. There seems to be some race condition in there. I've added a manual timeout in there - tweak its numbers to see different behavior. In the code the timeout is 1 second and then one of the tests fail almost every time. Increasing the timeoyt will often make both tests fail
What is the expected behavior?
All linking and execution should happen before the promises resolve.
What do you see instead?
The promises resolve, so the internal state of
completed
is set totrue
, which later throws.Additional information
The reproduction is adapted from a bug report to Jest (jestjs/jest#9430 (comment)), so apologies if it looks a little wonky. I've tried to emulate sorta what happens under the hood in Jest down to solely node core modules.
Note that I might very well have gotten some semantics wrong in the dynamic linking, so please tell me if I'm doing something really dumb in the reproduction (except having no module cache at all) - I've probably made the same mistake in Jest's implementation. Main difference is that the calls will linger - In jest these would all be inside
test()
calls or some such that we execute later. I don't think it impacts the reproduction much though - the promises resolve seconds before linking complete, so I don't think it's necessarily tied to dangling promises. I might very well be wrong though.For some extra context if it's helpful, here's Jest's implementation: https://github.com/facebook/jest/blob/7a3c9977847cc9fafed6f6662289f3c35e44e0c6/packages/jest-runtime/src/index.ts#L323-L456
It's spread a bit around, but those are the essential bits where we use the VM APIs.
The text was updated successfully, but these errors were encountered: