Skip to content

Commit 3d1bdc2

Browse files
rickyesaddaleax
authored andcommittedSep 28, 2020
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: nodejs/node#33617 Backport-PR-URL: nodejs/node#35396 Fixes: nodejs/node#31942 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent b9d0f73 commit 3d1bdc2

File tree

3 files changed

+177
-4
lines changed

3 files changed

+177
-4
lines changed
 

‎doc/api/http.md

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

288+
### `agent.maxTotalSockets`
289+
<!-- YAML
290+
added: REPLACEME
291+
-->
292+
293+
* {number}
294+
295+
By default set to `Infinity`. Determines how many concurrent sockets the agent
296+
can have open. Unlike `maxSockets`, this parameter applies across all origins.
297+
288298
### `agent.requests`
289299
<!-- YAML
290300
added: v0.5.9

‎lib/_http_agent.js

+54-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,
@@ -34,7 +35,15 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
3435
debug = fn;
3536
});
3637
const { async_id_symbol } = require('internal/async_hooks').symbols;
38+
const {
39+
codes: {
40+
ERR_OUT_OF_RANGE,
41+
},
42+
} = require('internal/errors');
43+
const { validateNumber } = require('internal/validators');
44+
3745
const kOnKeylog = Symbol('onkeylog');
46+
const kRequestOptions = Symbol('requestOptions');
3847
// New Agent code.
3948

4049
// The largest departure from the previous implementation is that
@@ -81,6 +90,17 @@ function Agent(options) {
8190
this.keepAlive = this.options.keepAlive || false;
8291
this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets;
8392
this.maxFreeSockets = this.options.maxFreeSockets || 256;
93+
this.maxTotalSockets = this.options.maxTotalSockets;
94+
this.totalSocketCount = 0;
95+
96+
if (this.maxTotalSockets !== undefined) {
97+
validateNumber(this.maxTotalSockets, 'maxTotalSockets');
98+
if (this.maxTotalSockets <= 0 || NumberIsNaN(this.maxTotalSockets))
99+
throw new ERR_OUT_OF_RANGE('maxTotalSockets', '> 0',
100+
this.maxTotalSockets);
101+
} else {
102+
this.maxTotalSockets = Infinity;
103+
}
84104

85105
this.on('free', (socket, options) => {
86106
const name = this.getName(options);
@@ -113,7 +133,9 @@ function Agent(options) {
113133
if (this.sockets[name])
114134
count += this.sockets[name].length;
115135

116-
if (count > this.maxSockets || freeLen >= this.maxFreeSockets) {
136+
if (this.totalSocketCount > this.maxTotalSockets ||
137+
count > this.maxSockets ||
138+
freeLen >= this.maxFreeSockets) {
117139
socket.destroy();
118140
} else if (this.keepSocketAlive(socket)) {
119141
freeSockets = freeSockets || [];
@@ -236,7 +258,9 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
236258
this.reuseSocket(socket, req);
237259
setRequestSocket(this, req, socket);
238260
this.sockets[name].push(socket);
239-
} else if (sockLen < this.maxSockets) {
261+
this.totalSocketCount++;
262+
} else if (sockLen < this.maxSockets &&
263+
this.totalSocketCount < this.maxTotalSockets) {
240264
debug('call onSocket', sockLen, freeLen);
241265
// If we are under maxSockets create a new one.
242266
this.createSocket(req, options, handleSocketCreation(this, req, true));
@@ -246,6 +270,10 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
246270
if (!this.requests[name]) {
247271
this.requests[name] = [];
248272
}
273+
274+
// Used to create sockets for pending requests from different origin
275+
req[kRequestOptions] = options;
276+
249277
this.requests[name].push(req);
250278
}
251279
};
@@ -275,7 +303,8 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) {
275303
this.sockets[name] = [];
276304
}
277305
this.sockets[name].push(s);
278-
debug('sockets', name, this.sockets[name].length);
306+
this.totalSocketCount++;
307+
debug('sockets', name, this.sockets[name].length, this.totalSocketCount);
279308
installListeners(this, s, options);
280309
cb(null, s);
281310
};
@@ -376,17 +405,38 @@ Agent.prototype.removeSocket = function removeSocket(s, options) {
376405
// Don't leak
377406
if (sockets[name].length === 0)
378407
delete sockets[name];
408+
this.totalSocketCount--;
379409
}
380410
}
381411
}
382412

413+
let req;
383414
if (this.requests[name] && this.requests[name].length) {
384415
debug('removeSocket, have a request, make a socket');
385-
const req = this.requests[name][0];
416+
req = this.requests[name][0];
417+
} else {
418+
// TODO(rickyes): this logic will not be FIFO across origins.
419+
// There might be older requests in a different origin, but
420+
// if the origin which releases the socket has pending requests
421+
// that will be prioritized.
422+
for (const prop in this.requests) {
423+
// Check whether this specific origin is already at maxSockets
424+
if (this.sockets[prop] && this.sockets[prop].length) break;
425+
debug('removeSocket, have a request with different origin,' +
426+
' make a socket');
427+
req = this.requests[prop][0];
428+
options = req[kRequestOptions];
429+
break;
430+
}
431+
}
432+
433+
if (req && options) {
434+
req[kRequestOptions] = undefined;
386435
// If we have pending requests and a socket gets closed make a new one
387436
const socketCreationHandler = handleSocketCreation(this, req, false);
388437
this.createSocket(req, options, socketCreationHandler);
389438
}
439+
390440
};
391441

392442
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)
Failed to load comments.