net: do not add multiple connect and finish listeners when multiple Socket.destroySoon's were scheduled#60469
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:
|
| 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.
I don't think this guard is need, wriable._final() is only called once.
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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".
…eners.js Co-authored-by: Luigi Pinca <luigipinca@gmail.com>
…eners.js Co-authored-by: Luigi Pinca <luigipinca@gmail.com>
| else | ||
| this.once('finish', this.destroy); | ||
| else { | ||
| this.destroyingOnFinish = true; |
There was a problem hiding this comment.
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 详细说明修复方案
|
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.