Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -529,8 +529,9 @@ Socket.prototype._unrefTimer = function _unrefTimer() {
// sent out to the other side.
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
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
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
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
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.

debug('_final: not yet connected');
this._finalizingOnConnect = true;
return this.once('connect', () => this._final(cb));
}

Expand Down Expand Up @@ -800,8 +801,10 @@ Socket.prototype.destroySoon = function() {

if (this.writableFinished)
this.destroy();
else
else if (!this._destroyingOnFinish) {
this._destroyingOnFinish = true;
this.once('finish', this.destroy);
}
};


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';
require('../common');
const { addresses } = require('../common/internet');

const assert = require('assert');
const net = require('net');

const socket = new net.Socket();
socket.on('error', () => {
// noop
});
socket.connect({ host: addresses.INVALID_HOST, port: 1234 });
socket.destroySoon();
socket.destroySoon();
const finishListenersCount = socket.listeners('finish').length;
const connectListenersCount = socket.listeners('connect').length;
assert.strictEqual(finishListenersCount, 1);
assert.strictEqual(connectListenersCount, 1);
Loading