Skip to content

Commit

Permalink
http: added scheduling option to http agent
Browse files Browse the repository at this point in the history
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 <[email protected]>
Reviewed-By: Matteo Collina <[email protected]>
  • Loading branch information
delvedor authored and MylesBorins committed Nov 16, 2020
1 parent d477e2e commit cce4645
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 1 deletion.
16 changes: 16 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ changes:
- version: v12.19.0
pr-url: https://github.com/nodejs/node/pull/33617
description: Add `maxTotalSockets` option to agent constructor.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/33278
description: Add `scheduling` option to specify the free socket
scheduling strategy.
-->

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

Expand Down
11 changes: 10 additions & 1 deletion lib/_http_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const { async_id_symbol } = require('internal/async_hooks').symbols;
const {
codes: {
ERR_OUT_OF_RANGE,
ERR_INVALID_OPT_VALUE,
},
} = require('internal/errors');
const { validateNumber } = require('internal/validators');
Expand Down Expand Up @@ -102,6 +103,12 @@ function Agent(options) {
this.maxTotalSockets = Infinity;
}

this.scheduling = this.options.scheduling || 'fifo';

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

this.on('free', (socket, options) => {
const name = this.getName(options);
debug('agent.on(free)', name);
Expand Down Expand Up @@ -238,7 +245,9 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
while (freeSockets.length && freeSockets[0].destroyed) {
freeSockets.shift();
}
socket = freeSockets.shift();
socket = this.scheduling === 'fifo' ?
freeSockets.shift() :
freeSockets.pop();
if (!freeSockets.length)
delete this.freeSockets[name];
}
Expand Down
148 changes: 148 additions & 0 deletions test/parallel/test-http-agent-scheduling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
'use strict';

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

function createServer(count) {
return http.createServer(common.mustCallAtLeast((req, res) => {
// Return the remote port number used for this connection.
res.end(req.socket.remotePort.toString(10));
}), count);
}

function makeRequest(url, agent, callback) {
http
.request(url, { agent }, (res) => {
let data = '';
res.setEncoding('ascii');
res.on('data', (c) => {
data += c;
});
res.on('end', () => {
process.nextTick(callback, data);
});
})
.end();
}

function bulkRequest(url, agent, done) {
const ports = [];
let count = agent.maxSockets;

for (let i = 0; i < agent.maxSockets; i++) {
makeRequest(url, agent, callback);
}

function callback(port) {
count -= 1;
ports.push(port);
if (count === 0) {
done(ports);
}
}
}

function defaultTest() {
const server = createServer(8);
server.listen(0, onListen);

function onListen() {
const url = `http://localhost:${server.address().port}`;
const agent = new http.Agent({
keepAlive: true,
maxSockets: 5
});

bulkRequest(url, agent, (ports) => {
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[0], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[1], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[2], port);
server.close();
agent.destroy();
});
});
});
});
}
}

function fifoTest() {
const server = createServer(8);
server.listen(0, onListen);

function onListen() {
const url = `http://localhost:${server.address().port}`;
const agent = new http.Agent({
keepAlive: true,
maxSockets: 5,
scheduling: 'fifo'
});

bulkRequest(url, agent, (ports) => {
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[0], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[1], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[2], port);
server.close();
agent.destroy();
});
});
});
});
}
}

function lifoTest() {
const server = createServer(8);
server.listen(0, onListen);

function onListen() {
const url = `http://localhost:${server.address().port}`;
const agent = new http.Agent({
keepAlive: true,
maxSockets: 5,
scheduling: 'lifo'
});

bulkRequest(url, agent, (ports) => {
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[ports.length - 1], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[ports.length - 1], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[ports.length - 1], port);
server.close();
agent.destroy();
});
});
});
});
}
}

function badSchedulingOptionTest() {
try {
new http.Agent({
keepAlive: true,
maxSockets: 5,
scheduling: 'filo'
});
} catch (err) {
assert.strictEqual(err.code, 'ERR_INVALID_OPT_VALUE');
assert.strictEqual(
err.message,
'The value "filo" is invalid for option "scheduling"'
);
}
}

defaultTest();
fifoTest();
lifoTest();
badSchedulingOptionTest();

0 comments on commit cce4645

Please sign in to comment.