Repository navigation
cluster.worker.on('message', (msg) => ...) fails to register a callback if ESM file extensions is used #48578
Description
Activity
Additional information: if example code for IPC with cluster is put the same ESM file, the callback for
'message'event is properly registered and called. Poolifier code is using theexecoption to specify the worker file path. And in that case,cluster.worker.on('',() => {})seems to have issues with ESM.Can you share a repro?
Please read the detailed bug report on poolifier repo, it contains all the bits to reproduce it reliably with poolifier. I do not have the time currently to extract the code from it to reproduce with two files using .mjs extension.
I don't have the time either, I don't plan on working on it without a repro that does not involve external code.
I'll close this until there is a standalone test case. Let me know when I should reopen.
The way that bug 100% reproducible has been handled by the node.js project is below the common sense standards expected if the project goal is stability.
When I receive a confirmed bug report 100% reproducible on one of the FOSS project I maintain, I would not even dare to put the burden on the bug reporter to solve it: it's unrespectful of the work done to identify the issue and pinpoint the root cause.The reason we don't usually accept bug reports that manifest in third-party code is that 9 out of 10 times the bug is in said third-party code, not Node.js.
Eventually comes the point where you decide you've wasted enough unpaid hours of your life on other people's crappy code.
Long story short: happy to take a look if you have a minimal test case. But if you are not willing to put in the time, then neither are we.
Reacted by Antoine du HamelPlease read carefully the bug report I've made before applying blindly rules that do not apply to it:
- CommonJS benchmarking code: everything works, callback registered with cluster.worker.on(), called in poolifier
- benchmarking code migrated to ESM: identical code, except the import part, using the very same (not even the ESM bundle, but the ESM bundle has the same issue) poolifier code previously working: callback not registered with cluster.worker.on() or called
=> Bug confirmed, 100% reproducible. Extracting a standalone test case is then becoming a corollary, not a prerequisite to its resolution. And then the burden is expected to be shared between project maintainers and the bug reporter.
Unless they're on your payroll, then no, you don't get to decide how and where other people invest their time.
Either put together a test case or don't, but stop arguing.
So the node.js project is not interested in releasing a stable ESM support in the cluster module by trying to welcome a proven 100% reproducible confirmed bug? And prefers to apply blindly rules instead of collaborating with the bug reporter to help planning fixing it and makes node.js better?
I will do the standalone test case when time permits. But given the way my first confirmed 100% reproducible bug report on the node.js project has been welcomed, I do not think I will redo it unless it impacts deeply a FOSS project I comaintain.
What about you? Are you not interested in releasing a stable ESM support in the cluster module? I think there's a misunderstanding here, the Node.js project is run by volunteers, you can't expect that someone else would be interested in investing their time in fixing your bug, especially when you yourself are not. Or you need to pay them (send me an email, I can make myself available).
The Node.js project would gladly accept a PR to fix it, but the Node.js project cannot open PRs by itself, only contributors can; and you are unlikely to find someone to spend time on it if they are not affected by the bug themselves. If you provide a repro – which is as you probably know is often the hardest part of fixing a bug – you're way more likely to find a volunteer.But given the way my first confirmed 100% reproducible bug report on the node.js project has been welcomed, I do not think I will redo it unless it impacts deeply a FOSS project I comaintain.
You'll be deeply missed. If I may suggest to tune down the entitlement and show a little more respect of other people's personal time, you could be amazed what difference it makes.
I do not expect any fix to be done, I do not expect any answer, I expect nothing from a bug report on a FOSS project, except one thing: that if taken into account, it's done fairly in depth by a volunteer that matters at fixing it and has the time to handle it. A sign of respect of the time spent by another volunteer to pointpoint the bug. If that's not the view of the node.js project volunteers, that's fine by me. But easily understandable that the volunteer in question will be reluctant at doing another detailed bug reports on the same project.
If the node.js project thinks that the present bug report has been handled in a respectful way and fairly, that discussion is going nowhere and there's no point at continuing it.Back to the subject: I'll do the test case reproducing the issue when time permits and attach the files to the bug report. And when times permit probably do a PR to try to fix it, as I usually do as a long time FOSS hacker on the open bug reports I do.
The test case:
- main.mjs:
import cluster from 'cluster' cluster.setupPrimary({ exec: './worker.mjs' }) const worker = cluster.fork() worker.on('message', message => { console.info('message received from worker:', message) }) worker.on('online', () => { console.info('worker is online') }) worker.on('error', (error) => { console.info('worker error', error) }) worker.on('disconnect', () => { console.info('worker disconnected') }) worker.on('exit', () => { console.info('worker exited') }) worker.send('hello')
- worker.mjs:
import cluster from 'cluster' cluster.worker.on('message', message => { console.info('echo message received from main:', message) cluster.worker.send(message) })
node main.mjsis frozen.Please reopen the issue.
- main.js:
const cluster = require('cluster') cluster.setupPrimary({ exec: './worker.js' }) const worker = cluster.fork() worker.on('message', message => { console.info('message received from worker:', message) }) worker.on('online', () => { console.info('worker is online') }) worker.on('error', (error) => { console.info('worker error', error) }) worker.on('disconnect', () => { console.info('worker disconnected') }) worker.on('exit', () => { console.info('worker exited') }) worker.send('hello')
- worker.js:
const cluster = require('cluster') cluster.worker.on('message', message => { console.info('echo message received from main:', message) cluster.worker.send(message) })
node main.jsjust works.Thanks, I'll reopen. Can you update your original report with the test cases?
I can make an educated guess as to why it doesn't work:
cluster.fork()is implemented on top ofchild_process.fork()child_process.fork()spawns the child process withNODE_CHANNEL_FD=<num>set in the environmentNODE_CHANNEL_FDmakes node's bootstrap code callchild_process._forkChild()to set up IPC- the asynchronous loading of worker.mjs makes that malfunction somehow
Interestingly, the worker sends the 'online' internal message. That's done by
cluster._setupWorker()and it's called fromlib/internal/process/pre_execution.jsso at least some of the steps are working, just not all.Anyway, you know where to start looking now. Pull request welcome.
- addedconfirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.clusterIssues and PRs related to the cluster subsystem.Issues and PRs related to the cluster subsystem.
on Jul 30, 2023 That will take quite a while before I will be able to work on it, so if someone starts to work on that issue, please say so and share your progress.
@fabiancook: thanks for your interest. You do need to use poolifier to fix that issue.
@mcollina i think this is quite a significant issue in terms of esm stability.
See @bnoordhuis last comment.
@mcollina i think this is quite a significant issue in terms of esm stability.
The issue is only met if the worker file is an ESM one. The main file type has no impact.
I'm facing the same issue
Reacted by electriquoThe issue is around for very long time. Would you please share an update?
Anyhow, I am including a simple example of the issue for reproduction.
Given the followingconst cluster = require("cluster"); // import cluster from "cluster"; if (cluster.isPrimary) { const worker = cluster.fork(); worker.on("message", (msg) => { console.log(`Worker to master: ${msg}`); }); worker.send("Hey worker") } else { process.on("message", (msg) => { console.log(`Master to worker: ${msg}`); }); process.send("Hey master"); }
When the first line is modified from
const cluster = require("cluster");toimport cluster from "cluster";, the message"Master to worker"is no longer received by the worker process, where as the expected behavior is that the worker should receive the"Master to worker"message regardless of whether theclustermodule is imported usingrequireorimport.Worth noting that the issue exists in NodeJS v22.14.0 and doesn't exist in Bun 1.2.4 :(
This issue has been open for nearly two years.
Does this mean it’s unlikely to be fixed?
Should we consider migrating away from Node.js in favor of a more actively maintained JavaScript runtime?Node.js is maintained by volunteers. If something is not fixed in a timely fashion, it's usually because no one volunteered to do it. You can always send a pull request to fix it yourself.
Reacted by electriquoI attempted to reproduce this issue on the current
mainbranch (v27.0.0-pre) and was unable to do so in any of the reported scenarios:process.on('message')inside a.mjsworker — works ✅cluster.worker.on('message')inside a.mjsworker — works ✅- Single
.mjsfile acting as both primary and worker — works ✅ worker.send()called immediately aftercluster.fork()(before'online') — works ✅
It appears this was silently fixed at some point between v22.14.0 and v27, likely as a side-effect of ESM loader refactoring.
Could someone verify whether this still affects the v22 or v20 LTS lines? If so, a backport may be needed. If it is confirmed fixed on
main, it would be good to close this issue or at least document the fix.@mhayk what are your results for #48578 (comment)?
Testing the specific scenario from your March 2025 comment on
main(v27.0.0-pre):import cluster from "cluster"; if (cluster.isPrimary) { const worker = cluster.fork(); worker.on("message", (msg) => { console.log(`Worker to primary: ${msg}`); }); worker.send("Hey worker"); } else { process.on("message", (msg) => { console.log(`Primary to worker: ${msg}`); process.send("Hey primary"); }); }
Output on
main:Primary to worker: Hey worker Worker to primary: Hey primaryBoth message callbacks are invoked correctly ✅. The process does not exit because there is no explicit
process.exit()call, the primary keeps running while the worker is alive. This is identical behaviour to the CJS equivalent usingrequire("cluster").The original bug (callbacks never being called) appears to be fixed on
main. The process hanging indefinitely is expected and not specific to ESM, it reproduces the same way with CJS.Reacted by electriquo
Version
v20.3.1
Platform
All supported platform
Subsystem
No response
What steps will reproduce the bug?
After a migration of code to ESM by using
.mjsfile extensions, the poolifier project has encountered issues at running internal benchmarks code.Test case:
node main.mjsis frozen.How often does it reproduce? Is there a required condition?
100% reproducible
What is the expected behavior? Why is that the expected behavior?
No response
What do you see instead?
Callback is never called if a message is sent from the primary.
Additional information
No response