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

Using custom loaders results in a load loop / OOM #47566

Closed
merceyz opened this issue Apr 14, 2023 · 21 comments · Fixed by #47620
Closed

Using custom loaders results in a load loop / OOM #47566

merceyz opened this issue Apr 14, 2023 · 21 comments · Fixed by #47620
Labels
confirmed-bug Issues with confirmed bugs. loaders Issues and PRs related to ES module loaders

Comments

@merceyz
Copy link
Member

merceyz commented Apr 14, 2023

Version

v20.0.0-nightly20230414c94be4125b

Platform

Linux server 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

Subsystem

No response

What steps will reproduce the bug?

touch loader.js
echo "setInterval(() => {}, 1000);" > test.cjs
node --loader ./loader.js test.cjs

How often does it reproduce? Is there a required condition?

No response

What is the expected behavior? Why is that the expected behavior?

Node.js starts up without loading the provided loader over and over again.

What do you see instead?

Node.js spams the console with the following warning while the memory and CPU usage climbs until it's stopped / OOMs.

(node:5652) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

Additional information

Possibly relevant #44710

@merceyz merceyz changed the title Combining --require and --loader results in a load loop Combining --require and --loader results in a load loop / OOM Apr 14, 2023
@aduh95
Copy link
Contributor

aduh95 commented Apr 15, 2023

@nodejs/loaders

@aduh95
Copy link
Contributor

aduh95 commented Apr 17, 2023

I'm able to reproduce with the suggested code snippet, but it looks like there's more to it than just combining --require and --loader, here's what I get when running the following at the root of the node repo:

$ out/Release/node --no-warnings --require ./test/fixtures/printA.js --import ./test/fixtures/printB.js --loader ./test/fixtures/empty.js ./test/fixtures/printC.js
A
A
B
C

It looks like the reported bug only happens on REPL. Worth noting that we also another bug that makes the --required script run twice instead of once.

@aduh95 aduh95 added the repl Issues and PRs related to the REPL subsystem. label Apr 17, 2023
@merceyz
Copy link
Member Author

merceyz commented Apr 17, 2023

It looks like the reported bug only happens on REPL

I can also reproduce the issue without the REPL:

touch require.cjs
touch loader.mjs
echo "setInterval(() => {}, 1000);" > test.cjs
node --require ./require.cjs --loader ./loader.mjs test.cjs

@aduh95
Copy link
Contributor

aduh95 commented Apr 17, 2023

I don't think it has much to do with --require:

$ out/Release/node --loader ./test/fixtures/empty.js
[…never ending output of experimental warning…]

@merceyz merceyz changed the title Combining --require and --loader results in a load loop / OOM Using custom loaders results in a load loop / OOM Apr 17, 2023
@merceyz
Copy link
Member Author

merceyz commented Apr 17, 2023

Indeed, I've updated the issue to reflect that 👍

Worth noting that we also another bug that makes the --required script run twice instead of once

That's expected behaviour in my opinion, once in the main process and once in the Worker for the loaders.

@JakobJingleheimer
Copy link
Member

I can't repro at all (admittedly on macOS ARM, but I don't see how this could be OS-related):

At main's current HEAD (e050cec), the OP's

snippet
touch loader.mjs
echo "setInterval(() => {}, 1000);" > test.cjs
node --loader ./loader.mjs test.cjs

runs endlessly (as expected) and memory stays constant (at 20.2 MB, after start-up), with only 1 experimental warning.

I get the same behaviour on v20.0.0-proposal's current HEAD (bb533f7), but with slightly less memory consumption (20.1 MB after start-up).

The code that controls whether the warning is emitted is quite simple and handles multiple instances of the module loader:

let emittedExperimentalWarning = false;
/**
* A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is
* only one used for loading the main module and everything in its dependency graph, though separate instances of this
* class might be instantiated as part of bootstrap for other purposes.
* @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them.
* @returns {DefaultModuleLoader | CustomizedModuleLoader}
*/
function createModuleLoader(useCustomLoadersIfPresent = true) {
if (useCustomLoadersIfPresent) {
const userLoaderPaths = getOptionValue('--experimental-loader');
if (userLoaderPaths.length > 0) {
if (!emittedExperimentalWarning) {
emitExperimentalWarning('Custom ESM Loaders');
emittedExperimentalWarning = true;
}

(emittedExperimentalWarning lives at the top level of esm/loader.js)

Am I missing something?

@aduh95
Copy link
Contributor

aduh95 commented Apr 17, 2023

I think this is happening because the loader is going to try to spawn a worker thread from within the worker thread, which is also going to do the same thing, and that's how we end up with Mr Meeseeks situation until the system is out of memory or the process terminates. I'm working on a fix.

@aduh95 aduh95 removed the repl Issues and PRs related to the REPL subsystem. label Apr 17, 2023
@aduh95 aduh95 added the confirmed-bug Issues with confirmed bugs. label Apr 17, 2023
@bmeck
Copy link
Member

bmeck commented Apr 17, 2023 via email

@kirrg001
Copy link

Experiencing the same. The loader is loaded over and over again as soon as I import specific libraries.

node --experimental-loader ./esm-loader.mjs index.mjs

esm-loader.mjs

// works
import { get } from 'node:https';

// both do not work and restart the loader in a loop
import CoffeeScript from 'coffeescript';
import mysql from 'mysql';

(node:9286) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)
(node:9286) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)
(node:9286) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)
(node:9286) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)

Works fine with Node v18

@merceyz
Copy link
Member Author

merceyz commented Apr 19, 2023

Indeed, I've updated the issue to reflect that 👍
#47566 (comment)

@aduh95 I tested again and the --require flag is required to reproduce, I've updated the reproduction again.
Seems I forgot that the terminal in VSCode adds NODE_OPTIONS with a --require flag for it's auto attach feature.

@merceyz merceyz changed the title Using custom loaders results in a load loop / OOM Combining --require and --loader results in a load loop / OOM Apr 19, 2023
@jacobq
Copy link
Contributor

jacobq commented Apr 19, 2023

Indeed, I've updated the issue to reflect that +1
#47566 (comment)

@aduh95 I tested again and the --require flag is required to reproduce, I've updated the reproduction again. Seems I forgot that the terminal in VSCode adds NODE_OPTIONS with a --require flag for it's auto attach feature.

How so? I am seeing the following in v20.0.0 (release) on x86_64 GNU/Linux, running from CLI and NODE_OPTIONS is not set, using blank/empty loader and script that just waits indefinitely so the process doesn't exit immediately:

$ touch empty.cjs # (does it / should it matter if we use .js .cjs or .mjs here??)
$ echo "setInterval(() => null, 1000);" > wait.js
$ node --loader ./empty.cjs wait.js
(node:668264) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:668264) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:668264) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:668264) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:668264) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
....

Passing --trace-warnings just add stack traces to each message:

(node:668371) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
    at emitExperimentalWarning (node:internal/util:273:11)
    at createModuleLoader (node:internal/modules/esm/loader:418:9)
    at loadESM (node:internal/process/esm_loader:18:19)
    at runMainESM (node:internal/modules/run_main:53:21)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:5)
    at node:internal/main/run_main_module:23:47
...

I mean....I guess the message did say that the feature could change at any time, but I am sure that this kind of change was not on purpose :)

@aduh95
Copy link
Contributor

aduh95 commented Apr 19, 2023

FWIW I'm also able to reproduce without --require. We already have a fix ready FYI.

@merceyz
Copy link
Member Author

merceyz commented Apr 19, 2023

How so?

@jacobq Interestingly it depends on the extensions used.

# OK
node --loader ./loader.mjs
# KO
node --loader ./loader.cjs
node --loader ./loader.js
node --require ./require.cjs --loader ./loader.mjs

@merceyz merceyz changed the title Combining --require and --loader results in a load loop / OOM Using custom loaders results in a load loop / OOM Apr 19, 2023
@JakobJingleheimer
Copy link
Member

JakobJingleheimer commented Apr 20, 2023

How so?

@jacobq Interestingly it depends on the extensions used.

# OK
node --loader ./loader.mjs
# KO
node --loader ./loader.cjs
node --loader ./loader.js
node --require ./require.cjs --loader ./loader.mjs

The reason is it must invoke the CJS module loader to repro (the default is CJS, so node --loader ./loader.js will default to CJS when nothing tells it otherwise, and the other 2 are explicitly CJS).

nodejs-github-bot pushed a commit that referenced this issue Apr 21, 2023
Use the default loader as the cascaded loader in the loader worker.
Otherwise we spawn loader workers in the loader workers indefinitely.

PR-URL: #47620
Fixes: #47566
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
@joacub
Copy link

joacub commented Apr 25, 2023

FWIW I'm also able to reproduce without --require. We already have a fix ready FYI.

when is this fix will be released ?, we are experiencing the same and we want to go over node 20 to have the improvements in cold imports that I already saw.

thanks

@checkrcarter
Copy link

Ditto here! Also curious when this will be released.

@GeoffreyBooth
Copy link
Member

Ditto here! Also curious when this will be released.

With 20.1.0 on 2023-05-02 per nodejs/Release#855, or in a patch release if one is done before then.

targos pushed a commit that referenced this issue May 2, 2023
Use the default loader as the cascaded loader in the loader worker.
Otherwise we spawn loader workers in the loader workers indefinitely.

PR-URL: #47620
Fixes: #47566
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
@TomasHubelbauer
Copy link

FYI 20.1.0 is out and the PR for this issue, #47620, is included in it.

@aheissenberger
Copy link

I tested with node 20.1.0 and the problem still exists without any changes:
cyco130/vavite#39 (comment)

@TomasHubelbauer
Copy link

Just in case this is useful to anyone - in my case it actually did fix the issue… But it also revealed another one which I think is related: #47880.

@kirrg001
Copy link

kirrg001 commented May 8, 2023

Thank you. 20.1.0 fixed the issue for us.

targos pushed a commit to targos/node that referenced this issue Nov 11, 2023
Use the default loader as the cascaded loader in the loader worker.
Otherwise we spawn loader workers in the loader workers indefinitely.

PR-URL: nodejs#47620
Fixes: nodejs#47566
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
targos pushed a commit that referenced this issue Nov 23, 2023
Use the default loader as the cascaded loader in the loader worker.
Otherwise we spawn loader workers in the loader workers indefinitely.

PR-URL: #47620
Backport-PR-URL: #50669
Fixes: #47566
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
sercher added a commit to sercher/graaljs that referenced this issue Apr 25, 2024
Use the default loader as the cascaded loader in the loader worker.
Otherwise we spawn loader workers in the loader workers indefinitely.

PR-URL: nodejs/node#47620
Backport-PR-URL: nodejs/node#50669
Fixes: nodejs/node#47566
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
sercher added a commit to sercher/graaljs that referenced this issue Apr 25, 2024
Use the default loader as the cascaded loader in the loader worker.
Otherwise we spawn loader workers in the loader workers indefinitely.

PR-URL: nodejs/node#47620
Backport-PR-URL: nodejs/node#50669
Fixes: nodejs/node#47566
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug Issues with confirmed bugs. loaders Issues and PRs related to ES module loaders
Projects
None yet