Skip to content

Commit cce4645

Browse files
delvedorMylesBorins
authored andcommittedNov 16, 2020
http: added scheduling option to http agent
In some cases, it is preferable to use a lifo scheduling strategy for the free sockets instead of default one, which is fifo. This commit introduces a scheduling option to add the ability to choose which strategy best fits your needs. Backport-PR-URL: #35649 PR-URL: #33278 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent d477e2e commit cce4645

File tree

3 files changed

+174
-1
lines changed

3 files changed

+174
-1
lines changed
 

‎doc/api/http.md

+16
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ changes:
116116
- version: v12.19.0
117117
pr-url: https://github.com/nodejs/node/pull/33617
118118
description: Add `maxTotalSockets` option to agent constructor.
119+
- version: REPLACEME
120+
pr-url: https://github.com/nodejs/node/pull/33278
121+
description: Add `scheduling` option to specify the free socket
122+
scheduling strategy.
119123
-->
120124

121125
* `options` {Object} Set of configurable options to set on the agent.
@@ -142,6 +146,18 @@ changes:
142146
* `maxFreeSockets` {number} Maximum number of sockets to leave open
143147
in a free state. Only relevant if `keepAlive` is set to `true`.
144148
**Default:** `256`.
149+
* `scheduling` {string} Scheduling strategy to apply when picking
150+
the next free socket to use. It can be `'fifo'` or `'lifo'`.
151+
The main difference between the two scheduling strategies is that `'lifo'`
152+
selects the most recently used socket, while `'fifo'` selects
153+
the least recently used socket.
154+
In case of a low rate of request per second, the `'lifo'` scheduling
155+
will lower the risk of picking a socket that might have been closed
156+
by the server due to inactivity.
157+
In case of a high rate of request per second,
158+
the `'fifo'` scheduling will maximize the number of open sockets,
159+
while the `'lifo'` scheduling will keep it as low as possible.
160+
**Default:** `'fifo'`.
145161
* `timeout` {number} Socket timeout in milliseconds.
146162
This will set the timeout when the socket is created.
147163

‎lib/_http_agent.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const { async_id_symbol } = require('internal/async_hooks').symbols;
3838
const {
3939
codes: {
4040
ERR_OUT_OF_RANGE,
41+
ERR_INVALID_OPT_VALUE,
4142
},
4243
} = require('internal/errors');
4344
const { validateNumber } = require('internal/validators');
@@ -102,6 +103,12 @@ function Agent(options) {
102103
this.maxTotalSockets = Infinity;
103104
}
104105

106+
this.scheduling = this.options.scheduling || 'fifo';
107+
108+
if (this.scheduling !== 'fifo' && this.scheduling !== 'lifo') {
109+
throw new ERR_INVALID_OPT_VALUE('scheduling', this.scheduling);
110+
}
111+
105112
this.on('free', (socket, options) => {
106113
const name = this.getName(options);
107114
debug('agent.on(free)', name);
@@ -238,7 +245,9 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
238245
while (freeSockets.length && freeSockets[0].destroyed) {
239246
freeSockets.shift();
240247
}
241-
socket = freeSockets.shift();
248+
socket = this.scheduling === 'fifo' ?
249+
freeSockets.shift() :
250+
freeSockets.pop();
242251
if (!freeSockets.length)
243252
delete this.freeSockets[name];
244253
}
+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
7+
function createServer(count) {
8+
return http.createServer(common.mustCallAtLeast((req, res) => {
9+
// Return the remote port number used for this connection.
10+
res.end(req.socket.remotePort.toString(10));
11+
}), count);
12+
}
13+
14+
function makeRequest(url, agent, callback) {
15+
http
16+
.request(url, { agent }, (res) => {
17+
let data = '';
18+
res.setEncoding('ascii');
19+
res.on('data', (c) => {
20+
data += c;
21+
});
22+
res.on('end', () => {
23+
process.nextTick(callback, data);
24+
});
25+
})
26+
.end();
27+
}
28+
29+
function bulkRequest(url, agent, done) {
30+
const ports = [];
31+
let count = agent.maxSockets;
32+
33+
for (let i = 0; i < agent.maxSockets; i++) {
34+
makeRequest(url, agent, callback);
35+
}
36+
37+
function callback(port) {
38+
count -= 1;
39+
ports.push(port);
40+
if (count === 0) {
41+
done(ports);
42+
}
43+
}
44+
}
45+
46+
function defaultTest() {
47+
const server = createServer(8);
48+
server.listen(0, onListen);
49+
50+
function onListen() {
51+
const url = `http://localhost:${server.address().port}`;
52+
const agent = new http.Agent({
53+
keepAlive: true,
54+
maxSockets: 5
55+
});
56+
57+
bulkRequest(url, agent, (ports) => {
58+
makeRequest(url, agent, (port) => {
59+
assert.strictEqual(ports[0], port);
60+
makeRequest(url, agent, (port) => {
61+
assert.strictEqual(ports[1], port);
62+
makeRequest(url, agent, (port) => {
63+
assert.strictEqual(ports[2], port);
64+
server.close();
65+
agent.destroy();
66+
});
67+
});
68+
});
69+
});
70+
}
71+
}
72+
73+
function fifoTest() {
74+
const server = createServer(8);
75+
server.listen(0, onListen);
76+
77+
function onListen() {
78+
const url = `http://localhost:${server.address().port}`;
79+
const agent = new http.Agent({
80+
keepAlive: true,
81+
maxSockets: 5,
82+
scheduling: 'fifo'
83+
});
84+
85+
bulkRequest(url, agent, (ports) => {
86+
makeRequest(url, agent, (port) => {
87+
assert.strictEqual(ports[0], port);
88+
makeRequest(url, agent, (port) => {
89+
assert.strictEqual(ports[1], port);
90+
makeRequest(url, agent, (port) => {
91+
assert.strictEqual(ports[2], port);
92+
server.close();
93+
agent.destroy();
94+
});
95+
});
96+
});
97+
});
98+
}
99+
}
100+
101+
function lifoTest() {
102+
const server = createServer(8);
103+
server.listen(0, onListen);
104+
105+
function onListen() {
106+
const url = `http://localhost:${server.address().port}`;
107+
const agent = new http.Agent({
108+
keepAlive: true,
109+
maxSockets: 5,
110+
scheduling: 'lifo'
111+
});
112+
113+
bulkRequest(url, agent, (ports) => {
114+
makeRequest(url, agent, (port) => {
115+
assert.strictEqual(ports[ports.length - 1], port);
116+
makeRequest(url, agent, (port) => {
117+
assert.strictEqual(ports[ports.length - 1], port);
118+
makeRequest(url, agent, (port) => {
119+
assert.strictEqual(ports[ports.length - 1], port);
120+
server.close();
121+
agent.destroy();
122+
});
123+
});
124+
});
125+
});
126+
}
127+
}
128+
129+
function badSchedulingOptionTest() {
130+
try {
131+
new http.Agent({
132+
keepAlive: true,
133+
maxSockets: 5,
134+
scheduling: 'filo'
135+
});
136+
} catch (err) {
137+
assert.strictEqual(err.code, 'ERR_INVALID_OPT_VALUE');
138+
assert.strictEqual(
139+
err.message,
140+
'The value "filo" is invalid for option "scheduling"'
141+
);
142+
}
143+
}
144+
145+
defaultTest();
146+
fifoTest();
147+
lifoTest();
148+
badSchedulingOptionTest();

0 commit comments

Comments
 (0)
Please sign in to comment.