From cce464513ece26a4d1dc8e14f5cfd2cb0a0c55ab Mon Sep 17 00:00:00 2001 From: delvedor Date: Thu, 7 May 2020 08:18:11 +0200 Subject: [PATCH] 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: https://github.com/nodejs/node/pull/35649 PR-URL: https://github.com/nodejs/node/pull/33278 Reviewed-By: Robert Nagy Reviewed-By: Matteo Collina --- doc/api/http.md | 16 +++ lib/_http_agent.js | 11 +- test/parallel/test-http-agent-scheduling.js | 148 ++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-http-agent-scheduling.js diff --git a/doc/api/http.md b/doc/api/http.md index 4186304da897a1..d44a349b0e6c6b 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -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. @@ -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. diff --git a/lib/_http_agent.js b/lib/_http_agent.js index 73b9ed2c32896e..875c01b201fb7b 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -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'); @@ -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); @@ -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]; } diff --git a/test/parallel/test-http-agent-scheduling.js b/test/parallel/test-http-agent-scheduling.js new file mode 100644 index 00000000000000..bcf07863b0fb61 --- /dev/null +++ b/test/parallel/test-http-agent-scheduling.js @@ -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();