Skip to content

Commit

Permalink
http: add maxTotalSockets to agent class
Browse files Browse the repository at this point in the history
Add maxTotalSockets to determine how many sockets an agent can open.
Unlike maxSockets, The maxTotalSockets does not count by per origin.

PR-URL: #33617
Fixes: #31942
Reviewed-By: Robert Nagy <[email protected]>
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: James M Snell <[email protected]>
rickyes authored and ronag committed Jun 21, 2020
1 parent 64d22c3 commit 7a5d3a2
Showing 3 changed files with 171 additions and 4 deletions.
10 changes: 10 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
@@ -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 `Infinity`. Determines how many concurrent sockets the agent
can have open. Unlike `maxSockets`, this parameter applies across all origins.

### `agent.requests`
<!-- YAML
added: v0.5.9
52 changes: 48 additions & 4 deletions lib/_http_agent.js
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@
'use strict';

const {
NumberIsNaN,
ObjectKeys,
ObjectSetPrototypeOf,
ObjectValues,
@@ -38,11 +39,14 @@ const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_OPT_VALUE,
ERR_OUT_OF_RANGE,
},
} = require('internal/errors');
const { once } = require('internal/util');
const { validateNumber } = require('internal/validators');

const kOnKeylog = Symbol('onkeylog');
const kRequestOptions = Symbol('requestOptions');
// New Agent code.

// The largest departure from the previous implementation is that
@@ -90,11 +94,22 @@ function Agent(options) {
this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets;
this.maxFreeSockets = this.options.maxFreeSockets || 256;
this.scheduling = this.options.scheduling || 'fifo';
this.maxTotalSockets = this.options.maxTotalSockets;
this.totalSocketCount = 0;

if (this.scheduling !== 'fifo' && this.scheduling !== 'lifo') {
throw new ERR_INVALID_OPT_VALUE('scheduling', this.scheduling);
}

if (this.maxTotalSockets !== undefined) {
validateNumber(this.maxTotalSockets, 'maxTotalSockets');
if (this.maxTotalSockets <= 0 || NumberIsNaN(this.maxTotalSockets))
throw new ERR_OUT_OF_RANGE('maxTotalSockets', '> 0',
this.maxTotalSockets);
} else {
this.maxTotalSockets = Infinity;
}

this.on('free', (socket, options) => {
const name = this.getName(options);
debug('agent.on(free)', name);
@@ -133,7 +148,8 @@ function Agent(options) {
if (this.sockets[name])
count += this.sockets[name].length;

if (count > this.maxSockets ||
if (this.totalSocketCount > this.maxTotalSockets ||
count > this.maxSockets ||
freeLen >= this.maxFreeSockets ||
!this.keepSocketAlive(socket)) {
socket.destroy();
@@ -248,7 +264,9 @@ 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) {
this.totalSocketCount++;
} else if (sockLen < this.maxSockets &&
this.totalSocketCount < this.maxTotalSockets) {
debug('call onSocket', sockLen, freeLen);
// If we are under maxSockets create a new one.
this.createSocket(req, options, (err, socket) => {
@@ -263,6 +281,10 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
if (!this.requests[name]) {
this.requests[name] = [];
}

// Used to create sockets for pending requests from different origin
req[kRequestOptions] = options;

this.requests[name].push(req);
}
};
@@ -288,7 +310,8 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) {
this.sockets[name] = [];
}
this.sockets[name].push(s);
debug('sockets', name, this.sockets[name].length);
this.totalSocketCount++;
debug('sockets', name, this.sockets[name].length, this.totalSocketCount);
installListeners(this, s, options);
cb(null, s);
});
@@ -394,13 +417,33 @@ Agent.prototype.removeSocket = function removeSocket(s, options) {
// Don't leak
if (sockets[name].length === 0)
delete sockets[name];
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 {
// TODO(rickyes): this logic will not be FIFO across origins.
// There might be older requests in a different origin, but
// if the origin which releases the socket has pending requests
// that will be prioritized.
for (const prop in this.requests) {
// Check whether this specific origin is already at maxSockets
if (this.sockets[prop] && this.sockets[prop].length) break;
debug('removeSocket, have a request with different origin,' +
' make a socket');
req = this.requests[prop][0];
options = req[kRequestOptions];
break;
}
}

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

};

Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
113 changes: 113 additions & 0 deletions test/parallel/test-http-agent-maxtotalsockets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const http = require('http');
const Countdown = require('../common/countdown');

assert.throws(() => new http.Agent({
maxTotalSockets: 'test',
}), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "maxTotalSockets" argument must be of type number. ' +
"Received type string ('test')",
});

[-1, 0, NaN].forEach((item) => {
assert.throws(() => new http.Agent({
maxTotalSockets: item,
}), {
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
message: 'The value of "maxTotalSockets" is out of range. ' +
`It must be > 0. Received ${item}`,
});
});

assert.ok(new http.Agent({
maxTotalSockets: Infinity,
}));

function start(param = {}) {
const { maxTotalSockets, maxSockets } = param;

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

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

server.keepAliveTimeout = 0;
server2.keepAliveTimeout = 0;

const countdown = new Countdown(12, () => {
assert.strictEqual(getRequestCount(), 0);
agent.destroy();
server.close();
server2.close();
});

function handler(s) {
for (let i = 0; i < 6; i++) {
http.get({
host: 'localhost',
port: s.address().port,
agent,
path: `/${i}`,
}, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
res.resume();
res.on('end', common.mustCall(() => {
for (const key of Object.keys(agent.sockets)) {
assert(agent.sockets[key].length <= maxSockets);
}
assert(getTotalSocketsCount() <= 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(server)));
server2.listen(0, common.mustCall(() => handler(server2)));
}

// If maxTotalSockets is larger than maxSockets,
// then the origin check will be skipped
// when the socket is removed.
[{
maxTotalSockets: 2,
maxSockets: 3,
}, {
maxTotalSockets: 3,
maxSockets: 2,
}, {
maxTotalSockets: 2,
maxSockets: 2,
}].forEach(start);

0 comments on commit 7a5d3a2

Please sign in to comment.