-
-
Notifications
You must be signed in to change notification settings - Fork 34.2k
net: do not add multiple connect and finish listeners when multiple Socket.destroySoon's were scheduled
#60469
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
base: main
Are you sure you want to change the base?
Conversation
|
Review requested:
|
524d5c5 to
8473258
Compare
8473258 to
6e33069
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #60469 +/- ##
==========================================
- Coverage 88.58% 88.54% -0.05%
==========================================
Files 704 704
Lines 207826 207886 +60
Branches 40054 40050 -4
==========================================
- Hits 184106 184071 -35
- Misses 15761 15844 +83
- Partials 7959 7971 +12
🚀 New features to boost your workflow:
|
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 1There was a problem hiding this comment.
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 2and 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); |
There was a problem hiding this comment.
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 3There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
test/parallel/test-net-socket-not-duplicates-destroy-soon-listeners.js
Outdated
Show resolved
Hide resolved
test/parallel/test-net-socket-not-duplicates-destroy-soon-listeners.js
Outdated
Show resolved
Hide resolved
…eners.js Co-authored-by: Luigi Pinca <luigipinca@gmail.com>
…eners.js Co-authored-by: Luigi Pinca <luigipinca@gmail.com>
lib/net.js
Outdated
| else | ||
| this.once('finish', this.destroy); | ||
| else { | ||
| this.destroyingOnFinish = true; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right 👍 Symbol used ✅
问题: - 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
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
|
Hi :) I suppose that tests do not pass because of flakiness :/ May I please to add |
Fixes #60456 by adding new
finishevent listener aftersocket.destroySooncall only if they were not added previously on given socket.