From 23aa364757eed719b7f19bfa3949144bf3bac7e1 Mon Sep 17 00:00:00 2001 From: Alex Liu Date: Wed, 11 Jul 2018 14:48:01 -0700 Subject: [PATCH] new: add inflightRequests() API --- README.md | 20 ++++ lib/HttpClient.js | 22 ++++- test/inflightRequests.js | 205 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 test/inflightRequests.js diff --git a/README.md b/README.md index 713ad24..0355967 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,7 @@ client.del('/foo/bar', function(err, req, res) { }); ``` + ### StringClient `StringClient` is what `JsonClient` is built on, and provides a base @@ -470,6 +471,25 @@ client.get(options, function(err, res, socket, head) { }); ``` +### More APIs + +The following methods are shared by all versions of the client: + +#### inflightRequests() + +Returns the number of inflight requests the client is currently handling. An +inflight request is a request that is in any of the following states: + +* connection establishment (dns resolution, socket connection, etc.) +* request serialization (uploading of req bodies) +* waiting for server response +* response marshalling/consumption + +The request count is incremented when a verb method is called, and is +decremented when the response's `end` event is emitted, or when the request's +`error` event is emitted. + + ### Events The client emits the following events: diff --git a/lib/HttpClient.js b/lib/HttpClient.js index aaee127..995a490 100644 --- a/lib/HttpClient.js +++ b/lib/HttpClient.js @@ -268,8 +268,8 @@ function rawRequest(opts, cb) { req.getMetrics = function getMetrics() { return metrics; }; + opts.client._inflightRequests--; opts.client.emit('metrics', metrics); - emitAfter(req, res, err); }); @@ -310,6 +310,7 @@ function rawRequest(opts, cb) { log.trace({ err: realErr }, 'Request failed'); clearTimeout(connectionTimer); clearTimeout(requestTimer); + opts.client._inflightRequests--; // the user provided callback is invoked as soon as a connection is // established. however, the request can be aborted or can fail after @@ -512,6 +513,10 @@ function HttpClient(options) { var self = this; + // internal only properties + this._inflightRequests = 0; + + // options properties this.agent = options.agent; this.appendPath = options.appendPath || false; this.ca = options.ca; @@ -788,6 +793,9 @@ HttpClient.prototype.request = function request(opts, cb) { assert.object(opts, 'options'); assert.func(cb, 'callback'); + var self = this; + self._inflightRequests++; + /* eslint-disable no-param-reassign */ cb = once.strict(cb); /* eslint-enable no-param-reassign */ @@ -947,3 +955,15 @@ HttpClient.prototype._options = function (method, options) { return (opts); }; + + +/** + * return number of currently inflight requests + * @public + * @method inflightRequests + * @returns {Number} +*/ +HttpClient.prototype.inflightRequests = function () { + var self = this; + return self._inflightRequests; +}; diff --git a/test/inflightRequests.js b/test/inflightRequests.js new file mode 100644 index 0000000..75ba8bc --- /dev/null +++ b/test/inflightRequests.js @@ -0,0 +1,205 @@ +'use strict'; + +// external files +var _ = require('lodash'); +var assert = require('chai').assert; +var bunyan = require('bunyan'); +var restify = require('restify'); + +// local files +var clients = require('../lib'); + + +describe('inflightRequests', function () { + + var SERVER; + var HTTPCLIENT = clients.createHttpClient({ + url: 'http://localhost:3000/', + requestTimeout: 100 + }); + var STRINGCLIENT = clients.createStringClient({ + url: 'http://localhost:3000/', + requestTimeout: 100 + }); + var LOG = bunyan.createLogger({ + name: 'clientlog' + }); + + beforeEach(function (done) { + assert.strictEqual(HTTPCLIENT.inflightRequests(), 0); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 0); + + SERVER = restify.createServer({ + name: 'unittest', + log: LOG + }); + SERVER.get('/200', function (req, res, next) { + res.send(200, { hello: 'world' }); + return next(); + }); + SERVER.get('/500', function (req, res, next) { + res.send(500, { empty: 'world' }); + return next(); + }); + SERVER.get('/timeout', function (req, res, next) { + setTimeout(function () { + return next(); + }, 1000); + }); + SERVER.use(restify.plugins.queryParser()); + SERVER.listen(3000, done); + }); + + afterEach(function (done) { + assert.strictEqual(HTTPCLIENT.inflightRequests(), 0); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 0); + + HTTPCLIENT.close(); + STRINGCLIENT.close(); + SERVER.close(done); + }); + + it('StringClient should increment and decrement inflight requests', + function (done) { + // request count decremented right before callback is fired. + STRINGCLIENT.get('/200', function (err, req, res, data) { + assert.ifError(err); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 0); + return done(); + }); + + // after firing one request + assert.strictEqual(STRINGCLIENT.inflightRequests(), 1); + }); + + it('StringClient should increment and decrement inflight on connection ' + + 'timeout', + function (done) { + // setup client to point to unresolvable IP + var client = clients.createStringClient({ + url: 'http://10.255.255.1/', + connectTimeout: 100, + retry: { + minTimeout: 100, + maxTimeout: 500, + // ensure even with retries we do correct counting + retries: 1 + } + }); + client.get('/foo', function (err, req, res, data) { + assert.strictEqual(err.name, 'ConnectTimeoutError'); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 0); + return done(); + }); + assert.strictEqual(client.inflightRequests(), 1); + }); + + it('StringClient should increment and decrement inflight on request ' + + 'timeout', function (done) { + // setup client to point to unresolvable IP + STRINGCLIENT.get('/timeout', function (err, req, res, data) { + assert.strictEqual(err.name, 'RequestTimeoutError'); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 0); + return done(); + }); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 1); + }); + + it('HttpClient should increment and decrement inflight requests', + function (done) { + HTTPCLIENT.get('/200', function (err, req) { + assert.ifError(err); + assert.strictEqual(HTTPCLIENT.inflightRequests(), 1); + + req.on('result', function (_err, res) { + assert.ifError(_err); + res.on('data', _.noop); + res.on('end', function () { + assert.strictEqual(HTTPCLIENT.inflightRequests(), 0); + return done(); + }); + }); + }); + }); + + it('HttpClient should increment and decrement connect timeout', + function (done) { + // setup client to point to unresolvable IP + var client = clients.createHttpClient({ + url: 'http://10.255.255.1/', + connectTimeout: 100, + retry: false + }); + + client.get('/timeout', function (err, req) { + assert.ok(err); + assert.strictEqual(err.name, 'ConnectTimeoutError'); + assert.strictEqual(client.inflightRequests(), 0); + return done(); + }); + + assert.strictEqual(client.inflightRequests(), 1); + }); + + it('HttpClient should increment and decrement request timeout', + function (done) { + HTTPCLIENT.get('/timeout', function (err, req) { + // no connect timeout + assert.ifError(err); + assert.strictEqual(HTTPCLIENT.inflightRequests(), 1); + + req.on('result', function (_err, res) { + assert.ok(_err); + assert.strictEqual(_err.name, 'RequestTimeoutError'); + assert.notOk(res); + assert.strictEqual(HTTPCLIENT.inflightRequests(), 0); + return done(); + }); + }); + }); + + it('HttpClient should increment and decrement on forced req abort', + function (done) { + + var client = clients.createHttpClient({ + url: 'http://localhost:3000' + }); + + client.get('/timeout', function (err, req) { + // no connect timeout + assert.ifError(err); + assert.strictEqual(client.inflightRequests(), 1); + + req.on('result', function (_err, res) { + assert.strictEqual(client.inflightRequests(), 0); + assert.ok(_err); + assert.ok(_err.message, 'socket hang up'); + assert.isNull(res); + return done(); + }); + + req.abort(); + }); + }); + + it('should count multiple inflight requests', function (done) { + + STRINGCLIENT.get('/200', _.noop); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 1); + + STRINGCLIENT.get('/200', _.noop); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 2); + + STRINGCLIENT.get('/200', _.noop); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 3); + + STRINGCLIENT.get('/200', _.noop); + assert.strictEqual(STRINGCLIENT.inflightRequests(), 4); + + setTimeout(function () { + // wait for all requests to complete + assert.strictEqual(STRINGCLIENT.inflightRequests(), 0); + return done(); + }, 500); + }); +});