Skip to content

net: do not add multiple connect and finish listeners when multiple Socket.destroySoon's were scheduled - #60469

Open
arturgawlik wants to merge 16 commits into
nodejs:mainfrom
arturgawlik:do-not-leak-when-destroying-socket-multiple-times
Open

net: do not add multiple connect and finish listeners when multiple Socket.destroySoon's were scheduled#60469
arturgawlik wants to merge 16 commits into
nodejs:mainfrom
arturgawlik:do-not-leak-when-destroying-socket-multiple-times

Conversation

@arturgawlik

@arturgawlik arturgawlik commented Oct 28, 2025

Copy link
Copy Markdown
Contributor

Fixes #60456 by adding new finish event listener after socket.destroySoon call only if they were not added previously on given socket.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/net

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. net Issues and PRs related to the net subsystem. labels Oct 28, 2025
@arturgawlik
arturgawlik force-pushed the do-not-leak-when-destroying-socket-multiple-times branch 2 times, most recently from 8473258 to 6e33069 Compare October 28, 2025 21:32
@arturgawlik
arturgawlik marked this pull request as ready for review October 28, 2025 22:04
@codecov

codecov Bot commented Oct 28, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.11%. Comparing base (f170888) to head (58a7f8d).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #60469      +/-   ##
==========================================
- Coverage   90.13%   90.11%   -0.03%     
==========================================
  Files         751      751              
  Lines      253629   253637       +8     
  Branches    47792    47794       +2     
==========================================
- Hits       228615   228566      -49     
- Misses      16269    16325      +56     
- Partials     8745     8746       +1     
Files with missing lines Coverage Δ
lib/net.js 94.57% <100.00%> (+0.01%) ⬆️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread lib/net.js Outdated
Socket.prototype._final = function(cb) {
// If still connecting - defer handling `_final` until 'connect' will happen
if (this.connecting) {
if (this.connecting && !this._finalizingOnConnect) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this guard is need, wriable._final() is only called once.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry, maybe I'm missing something here 🤔 but from what I have been debugged it seems that Socket._final() is called for every socket.destroySoon() call in described scenario in the linked issue #60456

P.S. I've updated implementation to return from this function even if _finalizingOnConnect is already true (as it was previously).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

const net = require('net');

const socket = net.createConnection({
  host: 'example.com',
  port: 80,
  lookup() {}
});

socket.destroySoon();
socket.destroySoon();
socket.destroySoon();
socket.destroySoon();
socket.destroySoon();

console.log(socket.listenerCount('connect')); // Prints 1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right. My test didn't reproduce mentioned issue well. I reworked it to do it better. So now it looks like this:

const net = require('net');

const socket = new net.Socket();
socket.on('error', () => {
  // noop
});
const connectOptions = { host: "example.com", port: 1234 };

socket.connect(connectOptions);
socket.destroySoon(); // Adds "connect" and "finish" event listeners when socket has "writable" state
socket.destroy(); // Makes imideditly socket again "writable"

socket.connect(connectOptions);
socket.destroySoon(); // Should not duplicate "connect" and "finish" event listeners

console.log(socket.listeners('finish').length); // Prints 2
console.log(socket.listeners('connect').length); // Prints also 2

and with it we can observe that second call to socket.destroySoon() adds additional event listeners for finish and connect events, which could lead to memory leak.

socket.destroySoon(); // Adds "connect" and "finish" event listeners when socket has "writable" state
socket.destroy(); // Makes imideditly socket again "writable"

socket.connect(connectOptions);

@lpinca lpinca Oct 30, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is what actually makes the socket writable again as it calls socket._undestroy(). It is not recommended to call socket.connect() on a previously destroyed socket as its state might not be well defined. The issue you are describing can also happen in other cases with this usage pattern, for example:

const net = require('net');

const options = {
  host: 'example.com',
  port: 80,
  lookup() {}
};

const socket = new net.Socket();

socket.connect(options);
socket.read();
socket.destroy();

socket.connect(options);
socket.read();
socket.destroy();

socket.connect(options);
socket.read();
socket.destroy();

console.log(socket.listenerCount('connect')); // Prints 3

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank You for Your review! But do You think that mentioned issue should be even addressed? If yes I will analyze it deeper to provide better test that is reproducing this issue.

@lpinca lpinca Oct 31, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should fix the issue where multiple 'finish' events are added if socket.destroySoon() is called multiple times on the same socket. A possible fix is to make socket.destroySoon() a noop after the first call.

@arturgawlik arturgawlik Nov 1, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've removed changes for connect event listener, and leaved for finish (in destroySoon). I didn't make socket.destroySoon() completely noop after first call, because I still want this function to work back as it should when finish event eventually is emitted, and calling socket.destroySoon() potentially becomes back legitimate thing. But please let me know if You meant something else by writing "a noop after the first call".

@arturgawlik
arturgawlik marked this pull request as draft November 1, 2025 00:10
@arturgawlik
arturgawlik marked this pull request as ready for review November 1, 2025 22:48
Comment thread test/parallel/test-net-socket-not-duplicates-destroy-soon-listeners.js Outdated
Comment thread test/parallel/test-net-socket-not-duplicates-destroy-soon-listeners.js Outdated
Comment thread lib/net.js Outdated
else
this.once('finish', this.destroy);
else {
this.destroyingOnFinish = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please use a Symbol for this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

right 👍 Symbol used ✅

Aniu456 pushed a commit to Aniu456/School-Forum-Back-End that referenced this pull request Nov 15, 2025
问题:
- Socket.io 在高并发连接/断开时抛出 MaxListenersExceededWarning
- EventEmitter 默认最多允许 10 个监听器,超过会触发警告
- 参考: nodejs/node#60469

解决方案:
1. 在 afterInit 中设置 Server/Engine/EIO 的最大监听器数量为无限制 (setMaxListeners(0))
2. 优化 handleDisconnect,确保在断开连接时清理所有事件监听器
3. 实现 OnModuleDestroy 接口,在模块销毁时清理所有 WebSocket 资源

修改内容:
- 更新 notifications.gateway.ts:
  * 添加 OnModuleDestroy 接口实现
  * 增强 afterInit 方法设置监听器限制
  * 优化 handleDisconnect 方法的事件清理
  * 实现 onModuleDestroy 方法进行全面的资源清理

测试:
- npm run build 成功编译 ✅
- 0 个 TypeScript 错误
- 适用于高并发场景(100+ 并发连接)

文档:
- 添加 docs/WEBSOCKET_MEMORY_LEAK_FIX.md 详细说明修复方案

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Nov 23, 2025
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Nov 23, 2025
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@arturgawlik

Copy link
Copy Markdown
Contributor Author

Hi :) I suppose that tests do not pass because of flakiness :/ May I please to add request-ci label by someone with such privilege?

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been marked as stale due to 90 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.

@github-actions github-actions Bot added the stale label Jul 28, 2026
@lpinca lpinca added the request-ci Add this label to start a Jenkins CI on a PR. label Jul 28, 2026
@github-actions github-actions Bot added request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. and removed request-ci Add this label to start a Jenkins CI on a PR. labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor
Failed to start CI
- Validating Jenkins credentials
✔  Jenkins credentials valid
- Querying data for job/node-test-pull-request/70389/
[SyntaxError: Unexpected token '<', ..."    
  https://github.com/nodejs/node/actions/runs/30332820681

@github-actions github-actions Bot removed the stale label Jul 29, 2026
@aduh95
aduh95 force-pushed the do-not-leak-when-destroying-socket-multiple-times branch from c9a2c5e to 58a7f8d Compare August 25, 2026 09:35
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95 aduh95 added author ready PRs that have at least one approval, no outstanding review comments, and a CI started. and removed request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. labels Aug 25, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs that have at least one approval, no outstanding review comments, and a CI started. needs-ci PRs that need a full CI run. net Issues and PRs related to the net subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

socket.destroySoon causes two EventEmitter memory leaks when repetitively using it on a socket that is trying to connect

5 participants