Skip to content

Commit

Permalink
Merge pull request #64 from dmcquay/auto-throttle
Browse files Browse the repository at this point in the history
automatic throttling
  • Loading branch information
dmcquay committed Apr 6, 2016
2 parents a503783 + 0b1d8ce commit cd48248
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 0 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,23 @@ You will also need to go here: http://docs.aws.amazon.com/AWSECommerceService/la
and click on one of the locale specific associate websites to sign up as an associate and get an associate ID,
which is required for all API calls.

## Throttling / Request Limits

By default, Amazon limits you to one request per second per IP. This limit increases with revenue performance. Learn
more here: http://docs.aws.amazon.com/AWSECommerceService/latest/DG/TroubleshootingApplications.html

To help you ensure you don't exceed the request limit, we provide an automatic throttling feature. By default, apac will
not throttle. To enable throttling, set the maxRequestsPerSecond param when constructing your OperationHelper.

```javascript
var opHelper = new OperationHelper({
awsId: '[YOUR AWS ID HERE]',
awsSecret: '[YOUR AWS SECRET HERE]',
assocId: '[YOUR ASSOCIATE TAG HERE]',
maxRequestsPerSecond: 1
});
```

## Contributing

Feel free to submit a pull request. If you'd like, you may discuss the change with me first by submitting an issue.
Expand Down
26 changes: 26 additions & 0 deletions lib/OperationHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ var RSH = require('./RequestSignatureHelper').RequestSignatureHelper,
http = require('http'),
xml2js = require('xml2js')

const getNowMillis = () => {
return (new Date()).getTime()
}

class OperationHelper {
constructor(params) {
params = params || {}
Expand All @@ -27,6 +31,11 @@ class OperationHelper {
this.baseUri = params.baseUri || OperationHelper.defaultBaseUri
this.xml2jsOptions = params.xml2jsOptions || {}

// throttling
this.maxRequestsPerSecond = params.maxRequestsPerSecond || 0
this._timeBetweenRequestsInMilliSeconds = 1 / this.maxRequestsPerSecond * 1000
this._nextAvailableRequestMillis = getNowMillis()

// set version
if (typeof(params.version) === 'string') OperationHelper.version = params.version
}
Expand Down Expand Up @@ -60,6 +69,23 @@ class OperationHelper {
}

execute(operation, params, callback) {
let nowMillis = getNowMillis()
if (this.maxRequestsPerSecond === 0) {
return this._execute(operation, params, callback)
} else if (nowMillis >= this._nextAvailableRequestMillis) {
this._nextAvailableRequestMillis = getNowMillis() + this._timeBetweenRequestsInMilliSeconds
return this._execute(operation, params, callback)
} else {
return new Promise((resolve) => {
setTimeout(() => {
resolve(this._execute(operation, params, callback))
}, this._nextAvailableRequestMillis - nowMillis)
this._nextAvailableRequestMillis += this._timeBetweenRequestsInMilliSeconds
})
}
}

_execute(operation, params, callback) {
if (typeof(operation) === 'undefined') {
throw new Error('Missing operation parameter')
}
Expand Down
63 changes: 63 additions & 0 deletions lib/OperationHelper.specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const RSH = require('./RequestSignatureHelper').RequestSignatureHelper

var OperationHelper = require('./OperationHelper').OperationHelper

const getNowMillis = () => {
return (new Date()).getTime()
}

describe('OperationHelper', function () {
const awsId = 'testAwsId'
Expand Down Expand Up @@ -341,5 +344,65 @@ describe('OperationHelper', function () {
})
})
})

context('when throttling is necessary', () => {
let opHelper, startTimeMillis

beforeEach(() => {
opHelper = new OperationHelper(Object.assign({}, baseParams, {
maxRequestsPerSecond: 10
}))

const buildReqAndResp = () => {
let responseMock = new EventEmitter()
responseMock.setEncoding = sinon.spy()

let requestMock = new EventEmitter()
requestMock.end = () => {
responseMock.emit('data', responseBody)
responseMock.emit('end')
}

return {
req: requestMock,
res: responseMock
}
}

sinon.stub(http, 'request')
const reqRes1 = buildReqAndResp()
const reqRes2 = buildReqAndResp()
const reqRes3 = buildReqAndResp()
http.request.onFirstCall().callsArgWith(1, reqRes1.res).returns(reqRes1.req)
http.request.onSecondCall().callsArgWith(1, reqRes2.res).returns(reqRes2.req)
http.request.onThirdCall().callsArgWith(1, reqRes3.res).returns(reqRes3.req)

sinon.stub(opHelper, 'generateUri').returns('testUri')

const operation = 'ItemSearch'
const params = {
'SearchIndex': 'Books',
'Keywords': 'harry potter',
'ResponseGroup': 'ItemAttributes,Offers'
}

startTimeMillis = getNowMillis()
return Promise.all([
opHelper.execute(operation, params),
opHelper.execute(operation, params),
opHelper.execute(operation, params)
])
})

afterEach(() => {
http.request.restore()
})

it('should take at least (1 / maxRequestsPerSecond) * (numOperations - 1) seconds to complete', () => {
const durationMillis = getNowMillis() - startTimeMillis
expect(durationMillis).to.be.at.least(200)
expect(durationMillis).to.be.at.most(300)
})
})
})
})

0 comments on commit cd48248

Please sign in to comment.