Skip to content

Commit 507a2ef

Browse files
rickyescodebytere
authored andcommitted
http: add maxTotalSockets to agent class
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]>
1 parent fc7cad8 commit 507a2ef

File tree

3 files changed

+171
-4
lines changed

3 files changed

+171
-4
lines changed

doc/api/http.md

+10
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,16 @@ added: v0.3.6
300300
By default set to `Infinity`. Determines how many concurrent sockets the agent
301301
can have open per origin. Origin is the returned value of [`agent.getName()`][].
302302

303+
### `agent.maxTotalSockets`
304+
<!-- YAML
305+
added: REPLACEME
306+
-->
307+
308+
* {number}
309+
310+
By default set to `Infinity`. Determines how many concurrent sockets the agent
311+
can have open. Unlike `maxSockets`, this parameter applies across all origins.
312+
303313
### `agent.requests`
304314
<!-- YAML
305315
added: v0.5.9

lib/_http_agent.js

+48-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
'use strict';
2323

2424
const {
25+
NumberIsNaN,
2526
ObjectKeys,
2627
ObjectSetPrototypeOf,
2728
ObjectValues,
@@ -36,11 +37,14 @@ const {
3637
codes: {
3738
ERR_INVALID_ARG_TYPE,
3839
ERR_INVALID_OPT_VALUE,
40+
ERR_OUT_OF_RANGE,
3941
},
4042
} = require('internal/errors');
4143
const { once } = require('internal/util');
44+
const { validateNumber } = require('internal/validators');
4245

4346
const kOnKeylog = Symbol('onkeylog');
47+
const kRequestOptions = Symbol('requestOptions');
4448
// New Agent code.
4549

4650
// The largest departure from the previous implementation is that
@@ -88,11 +92,22 @@ function Agent(options) {
8892
this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets;
8993
this.maxFreeSockets = this.options.maxFreeSockets || 256;
9094
this.scheduling = this.options.scheduling || 'fifo';
95+
this.maxTotalSockets = this.options.maxTotalSockets;
96+
this.totalSocketCount = 0;
9197

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

102+
if (this.maxTotalSockets !== undefined) {
103+
validateNumber(this.maxTotalSockets, 'maxTotalSockets');
104+
if (this.maxTotalSockets <= 0 || NumberIsNaN(this.maxTotalSockets))
105+
throw new ERR_OUT_OF_RANGE('maxTotalSockets', '> 0',
106+
this.maxTotalSockets);
107+
} else {
108+
this.maxTotalSockets = Infinity;
109+
}
110+
96111
this.on('free', (socket, options) => {
97112
const name = this.getName(options);
98113
debug('agent.on(free)', name);
@@ -131,7 +146,8 @@ function Agent(options) {
131146
if (this.sockets[name])
132147
count += this.sockets[name].length;
133148

134-
if (count > this.maxSockets ||
149+
if (this.totalSocketCount > this.maxTotalSockets ||
150+
count > this.maxSockets ||
135151
freeLen >= this.maxFreeSockets ||
136152
!this.keepSocketAlive(socket)) {
137153
socket.destroy();
@@ -246,7 +262,9 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
246262
this.reuseSocket(socket, req);
247263
setRequestSocket(this, req, socket);
248264
this.sockets[name].push(socket);
249-
} else if (sockLen < this.maxSockets) {
265+
this.totalSocketCount++;
266+
} else if (sockLen < this.maxSockets &&
267+
this.totalSocketCount < this.maxTotalSockets) {
250268
debug('call onSocket', sockLen, freeLen);
251269
// If we are under maxSockets create a new one.
252270
this.createSocket(req, options, (err, socket) => {
@@ -261,6 +279,10 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
261279
if (!this.requests[name]) {
262280
this.requests[name] = [];
263281
}
282+
283+
// Used to create sockets for pending requests from different origin
284+
req[kRequestOptions] = options;
285+
264286
this.requests[name].push(req);
265287
}
266288
};
@@ -286,7 +308,8 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) {
286308
this.sockets[name] = [];
287309
}
288310
this.sockets[name].push(s);
289-
debug('sockets', name, this.sockets[name].length);
311+
this.totalSocketCount++;
312+
debug('sockets', name, this.sockets[name].length, this.totalSocketCount);
290313
installListeners(this, s, options);
291314
cb(null, s);
292315
});
@@ -392,13 +415,33 @@ Agent.prototype.removeSocket = function removeSocket(s, options) {
392415
// Don't leak
393416
if (sockets[name].length === 0)
394417
delete sockets[name];
418+
this.totalSocketCount--;
395419
}
396420
}
397421
}
398422

423+
let req;
399424
if (this.requests[name] && this.requests[name].length) {
400425
debug('removeSocket, have a request, make a socket');
401-
const req = this.requests[name][0];
426+
req = this.requests[name][0];
427+
} else {
428+
// TODO(rickyes): this logic will not be FIFO across origins.
429+
// There might be older requests in a different origin, but
430+
// if the origin which releases the socket has pending requests
431+
// that will be prioritized.
432+
for (const prop in this.requests) {
433+
// Check whether this specific origin is already at maxSockets
434+
if (this.sockets[prop] && this.sockets[prop].length) break;
435+
debug('removeSocket, have a request with different origin,' +
436+
' make a socket');
437+
req = this.requests[prop][0];
438+
options = req[kRequestOptions];
439+
break;
440+
}
441+
}
442+
443+
if (req && options) {
444+
req[kRequestOptions] = undefined;
402445
// If we have pending requests and a socket gets closed make a new one
403446
this.createSocket(req, options, (err, socket) => {
404447
if (err)
@@ -407,6 +450,7 @@ Agent.prototype.removeSocket = function removeSocket(s, options) {
407450
socket.emit('free');
408451
});
409452
}
453+
410454
};
411455

412456
Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
const Countdown = require('../common/countdown');
7+
8+
assert.throws(() => new http.Agent({
9+
maxTotalSockets: 'test',
10+
}), {
11+
code: 'ERR_INVALID_ARG_TYPE',
12+
name: 'TypeError',
13+
message: 'The "maxTotalSockets" argument must be of type number. ' +
14+
"Received type string ('test')",
15+
});
16+
17+
[-1, 0, NaN].forEach((item) => {
18+
assert.throws(() => new http.Agent({
19+
maxTotalSockets: item,
20+
}), {
21+
code: 'ERR_OUT_OF_RANGE',
22+
name: 'RangeError',
23+
message: 'The value of "maxTotalSockets" is out of range. ' +
24+
`It must be > 0. Received ${item}`,
25+
});
26+
});
27+
28+
assert.ok(new http.Agent({
29+
maxTotalSockets: Infinity,
30+
}));
31+
32+
function start(param = {}) {
33+
const { maxTotalSockets, maxSockets } = param;
34+
35+
const agent = new http.Agent({
36+
keepAlive: true,
37+
keepAliveMsecs: 1000,
38+
maxTotalSockets,
39+
maxSockets,
40+
maxFreeSockets: 3
41+
});
42+
43+
const server = http.createServer(common.mustCall((req, res) => {
44+
res.end('hello world');
45+
}, 6));
46+
const server2 = http.createServer(common.mustCall((req, res) => {
47+
res.end('hello world');
48+
}, 6));
49+
50+
server.keepAliveTimeout = 0;
51+
server2.keepAliveTimeout = 0;
52+
53+
const countdown = new Countdown(12, () => {
54+
assert.strictEqual(getRequestCount(), 0);
55+
agent.destroy();
56+
server.close();
57+
server2.close();
58+
});
59+
60+
function handler(s) {
61+
for (let i = 0; i < 6; i++) {
62+
http.get({
63+
host: 'localhost',
64+
port: s.address().port,
65+
agent,
66+
path: `/${i}`,
67+
}, common.mustCall((res) => {
68+
assert.strictEqual(res.statusCode, 200);
69+
res.resume();
70+
res.on('end', common.mustCall(() => {
71+
for (const key of Object.keys(agent.sockets)) {
72+
assert(agent.sockets[key].length <= maxSockets);
73+
}
74+
assert(getTotalSocketsCount() <= maxTotalSockets);
75+
countdown.dec();
76+
}));
77+
}));
78+
}
79+
}
80+
81+
function getTotalSocketsCount() {
82+
let num = 0;
83+
for (const key of Object.keys(agent.sockets)) {
84+
num += agent.sockets[key].length;
85+
}
86+
return num;
87+
}
88+
89+
function getRequestCount() {
90+
let num = 0;
91+
for (const key of Object.keys(agent.requests)) {
92+
num += agent.requests[key].length;
93+
}
94+
return num;
95+
}
96+
97+
server.listen(0, common.mustCall(() => handler(server)));
98+
server2.listen(0, common.mustCall(() => handler(server2)));
99+
}
100+
101+
// If maxTotalSockets is larger than maxSockets,
102+
// then the origin check will be skipped
103+
// when the socket is removed.
104+
[{
105+
maxTotalSockets: 2,
106+
maxSockets: 3,
107+
}, {
108+
maxTotalSockets: 3,
109+
maxSockets: 2,
110+
}, {
111+
maxTotalSockets: 2,
112+
maxSockets: 2,
113+
}].forEach(start);

0 commit comments

Comments
 (0)