Skip to content
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

http: add maxTotalSockets to agent class #33617

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,16 @@ added: v0.3.6
By default set to `Infinity`. Determines how many concurrent sockets the agent
can have open per origin. Origin is the returned value of [`agent.getName()`][].

### `agent.maxTotalSockets`
<!-- YAML
added: REPLACEME
-->

* {number}

By default set to `undefined`. Determines how many concurrent sockets the agent
can have open. The `maxSockets` will not take effect when this parameter is set.

### `agent.requests`
<!-- YAML
added: v0.5.9
Expand Down
74 changes: 64 additions & 10 deletions lib/_http_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const {
},
} = require('internal/errors');
const { once } = require('internal/util');
const { validateNumber } = require('internal/validators');

const kOnKeylog = Symbol('onkeylog');
// New Agent code.
Expand Down Expand Up @@ -93,6 +94,12 @@ function Agent(options) {
throw new ERR_INVALID_OPT_VALUE('scheduling', this.scheduling);
}

if (this.options.maxTotalSockets !== undefined) {
validateNumber(this.options.maxTotalSockets, 'maxTotalSockets');
this.maxTotalSockets = this.options.maxTotalSockets;
this.totalSocketCount = 0;
rickyes marked this conversation as resolved.
Show resolved Hide resolved
}

this.on('free', (socket, options) => {
const name = this.getName(options);
debug('agent.on(free)', name);
Expand Down Expand Up @@ -127,12 +134,25 @@ function Agent(options) {

const freeSockets = this.freeSockets[name] || [];
const freeLen = freeSockets.length;
let count = freeLen;
if (this.sockets[name])
count += this.sockets[name].length;

if (count > this.maxSockets ||
freeLen >= this.maxFreeSockets ||
// The maxSockets is invalid when maxTotalSockets is set
if (this.maxTotalSockets) {
if (this.totalSocketCount > this.maxTotalSockets) {
socket.destroy();
return;
}
} else {
let count = freeLen;
if (this.sockets[name])
count += this.sockets[name].length;

if (count > this.maxSockets) {
socket.destroy();
return;
}
}

if (freeLen >= this.maxFreeSockets ||
!this.keepSocketAlive(socket)) {
socket.destroy();
return;
Expand Down Expand Up @@ -231,9 +251,6 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
delete this.freeSockets[name];
}

const freeLen = freeSockets ? freeSockets.length : 0;
const sockLen = freeLen + this.sockets[name].length;

if (socket) {
// Guard against an uninitialized or user supplied Socket.
const handle = socket._handle;
Expand All @@ -246,7 +263,23 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
this.reuseSocket(socket, req);
setRequestSocket(this, req, socket);
this.sockets[name].push(socket);
} else if (sockLen < this.maxSockets) {
if (this.maxTotalSockets)
this.totalSocketCount++;
return;
}

const freeLen = freeSockets ? freeSockets.length : 0;
const sockLen = freeLen + this.sockets[name].length;
let isWait = false;

// The maxSockets is invalid when maxTotalSockets is set
if (this.maxTotalSockets) {
this.totalSocketCount >= this.maxTotalSockets && (isWait = true);
} else {
sockLen >= this.maxSockets && (isWait = true);
}

if (isWait === false) {
debug('call onSocket', sockLen, freeLen);
// If we are under maxSockets create a new one.
this.createSocket(req, options, (err, socket) => {
Expand All @@ -261,6 +294,10 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
if (!this.requests[name]) {
this.requests[name] = [];
}

if (this.maxTotalSockets)
req._options = options;

this.requests[name].push(req);
}
};
Expand All @@ -286,6 +323,8 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) {
this.sockets[name] = [];
}
this.sockets[name].push(s);
if (this.maxTotalSockets)
this.totalSocketCount++;
debug('sockets', name, this.sockets[name].length);
installListeners(this, s, options);
cb(null, s);
Expand Down Expand Up @@ -392,13 +431,27 @@ Agent.prototype.removeSocket = function removeSocket(s, options) {
// Don't leak
if (sockets[name].length === 0)
delete sockets[name];
if (this.maxTotalSockets)
this.totalSocketCount--;
}
}
}

let req;
if (this.requests[name] && this.requests[name].length) {
debug('removeSocket, have a request, make a socket');
const req = this.requests[name][0];
req = this.requests[name][0];
} else if (this.maxTotalSockets) {
for (const prop in this.requests) {
rickyes marked this conversation as resolved.
Show resolved Hide resolved
debug('removeSocket, have a request with different origin, make a socket');
req = this.requests[prop][0];
rickyes marked this conversation as resolved.
Show resolved Hide resolved
options = req._options;
delete req._options;
break;
}
}
rickyes marked this conversation as resolved.
Show resolved Hide resolved

if (req && options) {
// If we have pending requests and a socket gets closed make a new one
this.createSocket(req, options, (err, socket) => {
if (err)
Expand All @@ -407,6 +460,7 @@ Agent.prototype.removeSocket = function removeSocket(s, options) {
socket.emit('free');
});
}

};

Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
Expand Down
65 changes: 65 additions & 0 deletions test/parallel/test-http-agent-maxtotalsockets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const Countdown = require('../common/countdown');

const maxTotalSockets = 3;

const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxTotalSockets,
maxSockets: 100,
maxFreeSockets: 3
});

const server = http.createServer(common.mustCall((req, res) => {
res.end('hello world');
}, 6));

server.keepAliveTimeout = 0;

const countdown = new Countdown(6, () => {
agent.destroy();
server.close();
});

function handler() {
for (let i = 0; i < 6; i++) {
http.get({
host: 'localhost',
port: server.address().port,
agent: agent,
rickyes marked this conversation as resolved.
Show resolved Hide resolved
path: `/${i}`,
// Setting different origins
family: i < 3 ? 4 : 6
}, common.mustCall(res => {
assert.strictEqual(res.statusCode, 200);
res.resume();
res.on('end', common.mustCall(() => {
const count = getTotalSocketsCount();
assert(count <= maxTotalSockets);
countdown.dec();
}));
}));
}
}

function getTotalSocketsCount() {
let num = 0;
for (const key of Object.keys(agent.sockets)) {
num += agent.sockets[key].length;
}
return num;
}

function getRequestCount() {
let num = 0;
for (const key of Object.keys(agent.requests)) {
num += agent.requests[key].length;
}
return num;
}

server.listen(0, common.mustCall(handler));