-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add exponential backoff to API client
The API client now retries retrieving the serviceKey via the initialization request in an exponential backoff loop. The exponential algorithm utilizes an error factor to spread out the retrials, so when multiple clients are started at the same time they are unlikely to issue any request after the initial one at the same time.
- Loading branch information
1 parent
b463c22
commit 0f2e4d5
Showing
13 changed files
with
423 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
'use strict' | ||
|
||
var http = require('http') | ||
var inherits = require('util').inherits | ||
|
||
function HttpError (statusCode, response) { | ||
Error.captureStackTrace && Error.captureStackTrace(this, this.constructor) | ||
this.message = String(statusCode) + ' - ' + http.STATUS_CODES[statusCode] | ||
this.statusCode = statusCode | ||
this.response = response | ||
} | ||
inherits(HttpError, Error) | ||
|
||
module.exports = HttpError |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
'use strict' | ||
|
||
var HttpError = require('./httpError') | ||
var exponentialRetry = require('../../utils/exponentialRetry') | ||
|
||
var DEFAULT_MAX_RETRIES = Infinity | ||
var DEFAULT_MAX_WAIT = 10 * 60 * 1000 | ||
var DEFAULT_EXP_SCALE = 0.828 | ||
var DEFAULT_LIN_SCALE = 150 | ||
var DEFAULT_ERR_SCALE = 0.24 // +-12% error | ||
|
||
function httpRetry (opts, cb) { | ||
opts = opts || {} | ||
var client = opts.client | ||
var payload = opts.payload | ||
var reqOpts = opts.reqOpts | ||
var errorFilter = opts.errorFilter | ||
var maxRetries = opts.maxRetries != null ? opts.maxRetries : DEFAULT_MAX_RETRIES | ||
var maxWait = opts.maxWait != null ? opts.maxWait : DEFAULT_MAX_WAIT | ||
|
||
function httpRequest (cb) { | ||
var completed = false | ||
var req = client.request(reqOpts, function (response) { | ||
completed = true | ||
if (response.statusCode >= 400) { | ||
return cb(new HttpError(response.statusCode), response) | ||
} | ||
return cb(null, response) | ||
}) | ||
req.on('error', function (err) { | ||
if (!completed) { | ||
completed = true | ||
return cb(err) | ||
} | ||
}) | ||
if (payload) { | ||
req.write(payload) | ||
} | ||
req.end() | ||
} | ||
return exponentialRetry({ | ||
maxRetries: maxRetries, | ||
maxWait: maxWait, | ||
expScale: DEFAULT_EXP_SCALE, | ||
linScale: DEFAULT_LIN_SCALE, | ||
errScale: DEFAULT_ERR_SCALE, | ||
errorFilter: errorFilter | ||
}, httpRequest, cb) | ||
} | ||
|
||
module.exports = httpRetry |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
'use strict' | ||
|
||
var http = require('http') | ||
var HttpError = require('./httpError') | ||
var httpRetry = require('./httpRetry') | ||
var expect = require('chai').expect | ||
var nock = require('nock') | ||
var bl = require('bl') | ||
|
||
describe('httpRetry', function (done) { | ||
it('retries', function (done) { | ||
nock('http://something.com') | ||
.post('/', 'data') | ||
.reply(500) | ||
nock('http://something.com') | ||
.post('/', 'data') | ||
.reply(200, 'response') | ||
|
||
this.sandbox.stub(global, 'setTimeout').callsFake(function (cb, int) { | ||
return process.nextTick(cb) | ||
}) | ||
|
||
httpRetry({ | ||
client: http, | ||
maxRetries: 1, | ||
reqOpts: { | ||
hostname: 'something.com', | ||
method: 'POST', | ||
path: '/' | ||
}, | ||
payload: 'data' | ||
}, function (err, data) { | ||
expect(err).to.not.exist | ||
data.pipe(bl(function (err, data) { | ||
expect(err).not.to.exist | ||
expect(data.toString()).to.eql('response') | ||
done() | ||
})) | ||
}) | ||
}) | ||
it('returns error', function (done) { | ||
nock('http://something.com') | ||
.post('/', 'data') | ||
.reply(500, 'bad') | ||
|
||
this.sandbox.stub(global, 'setTimeout').callsFake(function (cb, int) { | ||
return process.nextTick(cb) | ||
}) | ||
|
||
httpRetry({ | ||
client: http, | ||
maxRetries: 0, | ||
reqOpts: { | ||
hostname: 'something.com', | ||
method: 'POST', | ||
path: '/' | ||
}, | ||
payload: 'data' | ||
}, function (err, data) { | ||
expect(err).to.be.instanceof(HttpError) | ||
data.pipe(bl(function (err, data) { | ||
expect(err).to.not.exist | ||
expect(data.toString()).to.eql('bad') | ||
done() | ||
})) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
'use strict' | ||
|
||
var inherits = require('util').inherits | ||
|
||
function CompositeError (message, cause) { | ||
if (message instanceof Error) { | ||
message = '' | ||
cause = message | ||
} | ||
this.message = message ? message.toString() : '' | ||
this.cause = cause | ||
Error.captureStackTrace && Error.captureStackTrace(this, this.constructor) | ||
if (this.stack != null && this.cause instanceof Error && this.cause.stack != null) { | ||
this.stack += '\nCaused by: ' + this.cause.stack | ||
} | ||
} | ||
inherits(CompositeError, Error) | ||
|
||
module.exports = CompositeError |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
'use strict' | ||
|
||
var inherits = require('util').inherits | ||
var retry = require('async/retry') | ||
|
||
var DEFAULT_MAX_RETRIES = Infinity | ||
var DEFAULT_MAX_WAIT = Infinity | ||
var DEFAULT_EXP_SCALE = 1 | ||
var DEFAULT_LIN_SCALE = 1 | ||
var DEFAULT_TRANS = 0 | ||
var DEFAULT_ERR_SCALE = 0 | ||
var DEFAULT_ERR_TRANS = 0 | ||
|
||
function MaxRetriesExceededError (n, last) { | ||
Error.captureStackTrace && Error.captureStackTrace(this, this.constructor) | ||
this.message = 'Network request max retry limit reached after ' + n + ' attempts. Last error message was: ' + last.message | ||
if (this.stack && last.stack) { | ||
this.stack += '\nCaused by: ' + last.stack | ||
} | ||
} | ||
inherits(MaxRetriesExceededError, Error) | ||
|
||
function exponentialRetry (opts, task, cb) { | ||
if (typeof opts === 'function') { | ||
cb = task | ||
task = opts | ||
opts = {} | ||
} | ||
opts = opts || {} | ||
var maxRetries = opts.maxRetries != null ? opts.maxRetries : DEFAULT_MAX_RETRIES | ||
var maxWait = opts.maxWait != null ? opts.maxWait : DEFAULT_MAX_WAIT | ||
var expScale = opts.expScale != null ? opts.expScale : DEFAULT_EXP_SCALE | ||
var linScale = opts.linScale != null ? opts.linScale : DEFAULT_LIN_SCALE | ||
var trans = opts.trans != null ? opts.trans : DEFAULT_TRANS | ||
var errScale = opts.errScale != null ? opts.errScale : DEFAULT_ERR_SCALE | ||
var errTrans = opts.errTrans != null ? opts.errTrans : DEFAULT_ERR_TRANS | ||
var errorFilter = opts.errorFilter | ||
|
||
return retry({ | ||
times: maxRetries + 1, | ||
errorFilter: errorFilter, | ||
interval: function (i) { | ||
var wait = Math.exp((i - 1) * expScale) * linScale + trans | ||
if (wait > maxWait) { | ||
wait = maxWait | ||
} | ||
var rnd = 0.5 - Math.random() | ||
wait = wait + (wait * rnd * errScale) + errTrans | ||
var res = Math.floor(wait) | ||
return res | ||
} | ||
}, task, cb) | ||
} | ||
|
||
module.exports = exponentialRetry | ||
module.exports.MaxRetriesExceededError = MaxRetriesExceededError |
Oops, something went wrong.