Skip to content

Commit

Permalink
Merge 758250d into c736ca4
Browse files Browse the repository at this point in the history
  • Loading branch information
badaldesai committed Mar 2, 2020
2 parents c736ca4 + 758250d commit 6510818
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 15 deletions.
12 changes: 12 additions & 0 deletions history.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# v1.4.9 - 2020/02/27

* Added backoff mechanism to any request called with setting totalTimeout.

# v1.4.8 - 2020/01/29

* Handle JSON.parse failure

# v1.4.7 - 2020/01/14

* Add ability to get profile based on authentication headers

# v1.4.6 - 2018/11/02

* Fixed bug that were not allowing the use of a connection pool
Expand Down
51 changes: 41 additions & 10 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ const
DEFAULT_MAX_REDIRECT_COUNT = 5,
DEFAULT_RETRY_COUNT = 3,
DEFAULT_TIMEOUT = 30000,
DEFAULT_DELAY = 30,
EVENT_REDIRECT = 'redirect',
EVENT_REQUEST = 'request',
EVENT_RESPONSE = 'response',
EXPONENT = 2,
FIRST_TRY = 1,
HTTP_ERROR_CODE_THRESHHOLD = 400,
HTTP_ERROR_CODE_RETRY_THRESHHOLD = 500,
Expand All @@ -32,6 +34,7 @@ const
'headers',
'host',
'hostname',
'initialDelay',
'keepAlive',
'keepAliveMsecs',
'localAddress',
Expand All @@ -47,7 +50,8 @@ const
'rawStream',
'secure',
'socketPath',
'timeout'],
'timeout',
'totalTimeout'],
SECURE_PROTOCOL_RE = /^https/i;

// Ctor
Expand Down Expand Up @@ -133,6 +137,15 @@ module.exports = (function (self) {
return formatted;
}

function isRetry(options, startTime, tryCount) {
if (options.totalTimeout) {
const timeElapsed = (new Date()) - startTime;
return timeElapsed < options.totalTimeout;
}

return tryCount <= options.maxRetries;
}

function Request (settings) {
let
_this = this,
Expand Down Expand Up @@ -219,11 +232,25 @@ module.exports = (function (self) {
options.headers = options.headers || {};
tryCount = tryCount || FIRST_TRY;

if (options.totalTimeout) {
options.totalTimeout = Number(options.totalTimeout);
options.initialDelay = Number(options.initialDelay);
if (!options.initialDelay) {
options.initialDelay = DEFAULT_DELAY;
}
if (options.totalTimeout > options.timeout) {
options.totalTimeout = options.timeout;
}
}

let
exec,
redirectCount = 0;
redirectCount = 0,
retryWait = options.initialDelay || 0;

exec = new Promise(function (resolve, reject) {
const startTime = new Date();
const delay = (retryWait) => new Promise((res) => setTimeout(res, retryWait));
if (typeof data !== 'string') {
data = JSON.stringify(data);
}
Expand Down Expand Up @@ -336,16 +363,16 @@ module.exports = (function (self) {
res.on('data', (chunk) => (chunks.push(chunk)));

res.once('end', () => {
let
body = chunks.join(''),
retry =
context.statusCode >= HTTP_ERROR_CODE_RETRY_THRESHHOLD &&
tryCount <= options.maxRetries;
let body = chunks.join('');
const retry = context.statusCode >= HTTP_ERROR_CODE_RETRY_THRESHHOLD
&& isRetry(options, startTime, tryCount);

// handle retry if error code is above threshhold
if (retry) {
tryCount += 1;
return makeRequest();
retryWait = retryWait * EXPONENT;
return delay(retryWait)
.then(makeRequest);
}

// attempt to parse the body
Expand Down Expand Up @@ -383,9 +410,13 @@ module.exports = (function (self) {

req.on('error', (err) => {
// retry if below retry count threshhold
if (tryCount <= options.maxRetries) {
if (isRetry(options, startTime, tryCount)) {
tryCount += 1;
return makeRequest();
retryWait = retryWait * EXPONENT;
return delay(retryWait)
.then(() => {
return makeRequest();
});
}

return reject(err)
Expand Down
8 changes: 8 additions & 0 deletions lib/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ module.exports = (function (self) {
outputOptions.timeout = parseInt(inputOptions.timeout, 10);
}

if (!self.isEmpty(inputOptions.totalTimeout) && !isNaN(inputOptions.totalTimeout)) {
outputOptions.totalTimeout = parseInt(inputOptions.totalTimeout, 10);
}

if (!self.isEmpty(inputOptions.initialDelay) && !isNaN(inputOptions.initialDelay)) {
outputOptions.initialDelay = parseInt(inputOptions.initialDelay, 10);
}

return outputOptions;
};

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "playnetwork-sdk",
"version": "1.4.8",
"version": "1.4.9",
"contributors": [
{
"name": "Joshua Thomas",
Expand Down
20 changes: 18 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,25 +200,39 @@ The supported options are as follows:
* `content`
* `host` - the hostname of the content API
* `secure` - defaults to `true`, defines when the API uses TLS
* `totalTimeout` - In miliseconds to activate the expotential backoff mechanism
* `initialDelay` - Override the initial delay for 30ms.
* `key`
* `host` - the hostname of the key API
* `secure` - defaults to `true`, defines when the API uses TLS
* `totalTimeout` - In miliseconds to activate the expotential backoff mechanism
* `initialDelay` - Override the initial delay for 30ms.
* `cacheTokens`
* `music`
* `host` - the hostname of the music API
* `secure` - defaults to `true`, defines when the API uses TLS
* `totalTimeout` - In miliseconds to activate the expotential backoff mechanism
* `initialDelay` - Override the initial delay for 30ms.
* `playback`
* `host` - the hostname of the playback API
* `secure` - defaults to `true`, defines when the API uses TLS
* `totalTimeout` - In miliseconds to activate the expotential backoff mechanism
* `initialDelay` - Override the initial delay for 30ms.
* `player`
* `host` - the hostname of the playerservice app
* `secure` - defaults to `true`, defines when the API uses TLS
* `totalTimeout` - In miliseconds to activate the expotential backoff mechanism
* `initialDelay` - Override the initial delay for 30ms.
* `provision`
* `host` - the hostname of the provision API
* `secure` - defaults to `true`, defines when the API uses TLS
* `totalTimeout` - In miliseconds to activate the expotential backoff mechanism
* `initialDelay` - Override the initial delay for 30ms.
* `settings`
* `host` - the hostname
* `secure` -
* `secure` - defaults to `true`, defines when the API uses TLS
* `totalTimeout` - In miliseconds to activate the expotential backoff mechanism
* `initialDelay` - Override the initial delay for 30ms.

See the following example that configures the SDK for interaction with a sandbox PlayNetwork environment (**_note:** this is an example only).

Expand All @@ -227,7 +241,9 @@ var
playnetwork = require('playnetwork-sdk'),
options = {
content : {
host : 'sandbox-content-api.apps.playnetwork.com'
host : 'sandbox-content-api.apps.playnetwork.com',
host : 'sandbox-content-api.apps.playnetwork.com',
totalTimeout: 40000 //ms To activate exponential backoff mechanism.
},
key : {
host : 'sandbox-key-api.apps.playnetwork.com'
Expand Down
48 changes: 48 additions & 0 deletions test/lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,4 +489,52 @@ describe('request', () => {
});
});
});
describe('request methods', function () {
['delete', 'get', 'head', 'post', 'put'].forEach((method) => {
describe(`#${method}`, function () {
let
options = {
host : `test-${method}.playnetwork.com`,
totalTimeout: 4000,
initialDelay: 1,
secure : true
},
requestInfo,
req = new request.Request(options);

// capture request and response info
req.on('request', (info) => (requestInfo = info));

afterEach(function() {
nock.cleanAll();
requestInfo = undefined;
});
it('should properly retry on 500s and based on exponential backoff', function(done) {

// intercept outbound request
let responseBody = { message : 'overload', statusCode : 503 };

// fail twice
nock(`https://${options.host}`)[method]('/v0/tests/retry')
.times(3)
.reply(responseBody.statusCode, responseBody);

// succeed on 3rd retry
nock(`https://${options.host}`)[method]('/v0/tests/retry')
.times(1)
.reply(200);

return req[method](
{ path : '/v0/tests/retry' },
function (err, response) {
should.not.exist(err);
requestInfo.totalTimeout.should.be.equal(options.totalTimeout);
requestInfo.initialDelay.should.be.equal(options.initialDelay);

return done();
});
});
});
});
});
});
11 changes: 10 additions & 1 deletion test/lib/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ describe('validation', () => {
port : 8080,
rejectUnauthorized : true,
secure : true,
timeout : 60000
timeout : 60000,
totalTimeout: 5000,
initialDelay: 50
},
outputOptions = validation.applyOptionalParameters(inputOptions);

Expand All @@ -45,6 +47,10 @@ describe('validation', () => {
outputOptions.rejectUnauthorized.should.equal(true);
outputOptions.should.have.property('timeout');
outputOptions.timeout.should.equal(60000);
outputOptions.should.have.property('totalTimeout');
outputOptions.totalTimeout.should.equal(5000);
outputOptions.should.have.property('initialDelay');
outputOptions.initialDelay.should.equal(50);

should.not.exist(outputOptions.host);
should.not.exist(outputOptions.secure);
Expand Down Expand Up @@ -81,6 +87,9 @@ describe('validation', () => {
outputOptions.secure.should.equal(true);
outputOptions.should.have.property('timeout');
outputOptions.timeout.should.equal(60000);

should.not.exist(outputOptions.totalTimeout);
should.not.exist(outputOptions.initialDelay);
});
});

Expand Down

0 comments on commit 6510818

Please sign in to comment.