Skip to content

Commit

Permalink
Merge pull request #54 from thekemkid/rate-limiting
Browse files Browse the repository at this point in the history
added rate limiting options
  • Loading branch information
GlenTiki committed Jul 8, 2016
2 parents 792cc98 + 42905ce commit 103fa77
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 76 deletions.
13 changes: 11 additions & 2 deletions README.md
Expand Up @@ -68,9 +68,16 @@ Available options:
-B/--bailout NUM
The number of failures before initiating a bailout.
-M/--maxConnectionRequests NUM
The max number of requests to make per connection to the server
The max number of requests to make per connection to the server.
-O/--maxOverallRequests NUM
The max number of requests to make overall to the server
The max number of requests to make overall to the server.
-r/--connectionRate NUM
The max number of requests to make per second from an individual connection.
-R/--overallRate NUM
The max number of requests to make per second from an all connections.
connection rate will take precedence if both are set.
NOTE: if using rate limiting and a very large rate is entered which cannot be met,
Autocannon will do as many requests as possible per second.
-n/--no-progress
Don't render the progress bar. default: false.
-l/--latency
Expand Down Expand Up @@ -119,6 +126,8 @@ Start autocannon against the given target.
* `setupClient`: A `Function` which will be passed the `Client` object for each connection to be made. This can be used to customise each individual connection headers and body using the API shown below. The changes you make to the client in this function will take precedence over the default `body` and `headers` you pass in here. There is an example of this in the samples folder. _OPTIONAL_ default: `function noop () {}`.
* `maxConnectionRequests`: A `Number` stating the max requests to make per connection. `amount` takes precedence if both are set. _OPTIONAL_
* `maxOverallRequests`: A `Number` stating the max requests to make overall. Can't be less than `connections`. `maxConnectionRequests` takes precedence if both are set. _OPTIONAL_
* `connectionRate`: A `Number` stating the rate of requests to make per second from each individual connection. No rate limiting by default. _OPTIONAL_
* `overallRate`: A `Number` stating the rate of requests to make per second from all connections. `conenctionRate` takes precedence if both are set. No rate limiting by default. _OPTIONAL_
* `requests`: An `Array` of `Object`s which represents the sequence of requests to make while benchmarking. Can be used in conjunction with the `body`, `headers` and `method` params above. The `Object`s in this array can have `body`, `headers`, `method`, or `path` attributes, which overwrite those that are passed in this `opts` object. Therefore, the ones in this (`opts`) object take precedence and should be viewed as defaults. Check the samples folder for an example of how this might be used. _OPTIONAL_.
* `cb`: The callback which is called on completion of the benchmark. Takes the following params. _OPTIONAL_.
* `err`: If there was an error encountered with the run.
Expand Down
2 changes: 2 additions & 0 deletions autocannon.js
Expand Up @@ -30,6 +30,8 @@ function start () {
input: 'i',
maxConnectionRequests: 'M',
maxOverallRequests: 'O',
connectionRate: 'r',
overallRate: 'R',
renderProgressBar: 'progress',
title: 'T',
version: 'v',
Expand Down
11 changes: 9 additions & 2 deletions help.txt
Expand Up @@ -27,9 +27,16 @@ Available options:
-B/--bailout NUM
The number of failures before initiating a bailout.
-M/--maxConnectionRequests NUM
The max number of requests to make per connection to the server
The max number of requests to make per connection to the server.
-O/--maxOverallRequests NUM
The max number of requests to make overall to the server
The max number of requests to make overall to the server.
-r/--connectionRate NUM
The max number of requests to make per second from an individual connection.
-R/--overallRate NUM
The max number of requests to make per second from an all connections.
connection rate will take precedence if both are set.
NOTE: if using rate limiting and a very large rate is entered which cannot be met,
Autocannon will do as many requests as possible per second.
-n/--no-progress
Don't render the progress bar. default: false.
-l/--latency
Expand Down
33 changes: 26 additions & 7 deletions lib/httpClient.js
Expand Up @@ -22,9 +22,15 @@ function Client (opts) {
if (this.secure && this.opts.port === 80) this.opts.port = 443
this.parser = new HTTPParser(HTTPParser.RESPONSE)
this.requestIterator = new RequestIterator(opts.requests, opts)

// used for request limiting
this.reqsMade = 0
this.responseMax = opts.responseMax

// used for rate limiting
this.reqsMadeThisSecond = 0
this.rate = opts.rate

this.resData = new Array(opts.pipelining)
for (var i = 0; i < this.resData.length; i++) {
this.resData[i] = {
Expand Down Expand Up @@ -52,6 +58,14 @@ function Client (opts) {
this._connect()
}

if (this.rate) {
this.rateInterval = setInterval(() => {
this.reqsMadeThisSecond = 0
if (this.paused) this._doRequest(this.cer)
this.paused = false
}, 1000)
}

this.timeoutTicker = retimer(handleTimeout, this.timeout)
this.parser[HTTPParser.kOnHeaders] = () => {}
this.parser[HTTPParser.kOnHeadersComplete] = (opts) => {
Expand All @@ -71,8 +85,6 @@ function Client (opts) {

this.cer = this.cer === opts.pipelining - 1 ? 0 : this.cer++
this._doRequest(this.cer)

this.timeoutTicker.reschedule(this.timeout)
}

this._connect()
Expand Down Expand Up @@ -108,12 +120,18 @@ Client.prototype._connect = function () {

// rpi = request pipelining index
Client.prototype._doRequest = function (rpi) {
if (!this.destroyed && this.responseMax && this.reqsMade++ >= this.responseMax) {
this.destroy()
return
if (!this.rate || (this.rate && this.reqsMadeThisSecond++ < this.rate)) {
if (!this.destroyed && this.responseMax && this.reqsMade++ >= this.responseMax) {
this.destroy()
return
}

this.resData[rpi].startTime = process.hrtime()
this.conn.write(this.requestIterator.move())
this.timeoutTicker.reschedule(this.timeout)
} else {
this.paused = true
}
this.resData[rpi].startTime = process.hrtime()
this.conn.write(this.requestIterator.move())
}

Client.prototype._destroyConnection = function () {
Expand All @@ -128,6 +146,7 @@ Client.prototype.destroy = function () {
if (!this.destroyed) {
this.destroyed = true
this.timeoutTicker.clear()
if (this.rate) clearInterval(this.rateInterval)
this.emit('done')
this._destroyConnection()
}
Expand Down
134 changes: 69 additions & 65 deletions lib/run.js
Expand Up @@ -18,6 +18,8 @@ const defaultOptions = {
timeout: 10,
maxConnectionRequests: 0,
maxOverallRequests: 0,
connectionRate: 0,
overallRate: 0,
amount: 0,
requests: [{}]
}
Expand All @@ -40,66 +42,10 @@ function run (opts, cb) {

opts = xtend(defaultOptions, opts)

if (!opts.url) {
cb(new Error('url option required'))
return
}

if (typeof opts.duration !== 'number') {
if (/[a-zA-Z]/.exec(opts.duration)) opts.duration = timestring(opts.duration)
else opts.duration = Number(opts.duration.trim())
}

if (typeof opts.duration === 'number') {
if (opts.duration <= 0) {
cb(new Error('duration must be greater than 0'))
return
}
} else {
cb(new Error('duration entered was in an invalid format'))
return
}
// do error checking, if error, return
if (checkOptsForErrors()) return

if (opts.connections < 1) {
cb(new Error('connections factor can not be < 1'))
return
}

if (opts.pipelining < 1) {
cb(new Error('pipelining factor can not be < 1'))
return
}

if (opts.timeout <= 0) {
cb(new Error('timeout must be greater than 0'))
return
}

if (opts.bailout && opts.bailout < 1) {
cb(new Error('bailout threshold can not be < 1'))
return
}

if (opts.amount) {
if (opts.amount < 1) {
cb(new Error('amount can not be < 1'))
return
}
}

if (opts.maxConnectionRequests && opts.maxConnectionRequests < 1) {
cb(new Error('maxConnectionRequests can not be < 1'))
return
}

if (opts.maxOverallRequests) {
if (opts.maxOverallRequests < 1) {
cb(new Error('maxOverallRequests can not be < 1'))
return
}
}

// set it down here, so throwing over invalid opts and setting defaults etc.
// set tracker.opts here, so throwing over invalid opts and setting defaults etc.
// is done
tracker.opts = opts

Expand All @@ -111,7 +57,7 @@ function run (opts, cb) {
let timeouts = 0
let totalBytes = 0
let totalRequests = 0
let amount = opts.amount === Infinity ? 0 : opts.amount
let amount = opts.amount
let stop = false
let numRunning = opts.connections
let startTime = Date.now()
Expand All @@ -125,18 +71,20 @@ function run (opts, cb) {
url.setupClient = opts.setupClient
url.timeout = opts.timeout
url.requests = opts.requests

url.responseMax = amount || opts.maxConnectionRequests || opts.maxOverallRequests
url.rate = opts.connectionRate || opts.overallRate

let clients = []
for (let i = 0; i < opts.connections; i++) {
if (!amount && !opts.maxConnectionRequests && opts.maxOverallRequests &&
typeof opts.maxOverallRequests === 'number') {
if (!amount && !opts.maxConnectionRequests && opts.maxOverallRequests) {
url.responseMax = distributeNums(opts.maxOverallRequests, i)
}
if (amount && typeof amount === 'number') {
if (amount) {
url.responseMax = distributeNums(amount, i)
}
if (!opts.connectionRate && opts.overallRate) {
url.rate = distributeNums(opts.overallRate, i)
}

let client = new Client(url)
client.on('response', onResponse)
Expand Down Expand Up @@ -224,8 +172,64 @@ function run (opts, cb) {
}
}, 1000)

// will return true if error with opts entered
function checkOptsForErrors () {
if (!opts.url) {
cb(new Error('url option required'))
return true
}

if (typeof opts.duration !== 'number') {
if (/[a-zA-Z]/.exec(opts.duration)) opts.duration = timestring(opts.duration)
else opts.duration = Number(opts.duration.trim())
}

if (typeof opts.duration === 'number') {
if (lessThanZeroError(opts.duration, 'duration')) return true
} else {
cb(new Error('duration entered was in an invalid format'))
return true
}

if (lessThanOneError(opts.connections, 'connections')) return true
if (lessThanOneError(opts.pipelining, 'pipelining factor')) return true
if (greaterThanZeroError(opts.timeout, 'timeout')) return true
if (opts.bailout && lessThanOneError(opts.bailout, 'bailout threshold')) return true
if (opts.connectionRate && lessThanOneError(opts.connectionRate, 'connectionRate')) return true
if (opts.overallRate && lessThanOneError(opts.overallRate, 'bailout overallRate')) return true
if (opts.amount && lessThanOneError(opts.amount, 'amount')) return true
if (opts.maxConnectionRequests && lessThanOneError(opts.maxConnectionRequests, 'maxConnectionRequests')) return true
if (opts.maxOverallRequests && lessThanOneError(opts.maxOverallRequests, 'maxOverallRequests')) return true

function lessThanZeroError (x, label) {
if (x < 0) {
cb(new Error(`${label} can not be less than 0`))
return true
}
return false
}

function lessThanOneError (x, label) {
if (x < 1) {
cb(new Error(`${label} can not be less than 1`))
return true
}
return false
}

function greaterThanZeroError (x, label) {
if (x <= 0) {
cb(new Error(`${label} must be greater than 0`))
return true
}
return false
}

return false
} // checkOptsForErrors

return tracker
}
} // run

function histAsObj (hist, total) {
const result = {
Expand Down
32 changes: 32 additions & 0 deletions test/runRate.test.js
@@ -0,0 +1,32 @@
'use strict'

const test = require('tap').test
const run = require('../lib/run')
const helper = require('./helper')
const server = helper.startServer()

test('run should only send the expected number of requests per second', (t) => {
t.plan(6)

run({
url: `http://localhost:${server.address().port}`,
connections: 2,
overallRate: 10,
amount: 40
}, (err, res) => {
t.error(err)
t.equal(res.duration, 4, 'should have take 4 seconds to send 10 requests per seconds')
t.equal(res.requests.average, 10, 'should have sent 10 requests per second on average')
})

run({
url: `http://localhost:${server.address().port}`,
connections: 2,
connectionRate: 10,
amount: 40
}, (err, res) => {
t.error(err)
t.equal(res.duration, 2, 'should have taken 2 seconds to send 10 requests per connection with 2 connections')
t.equal(res.requests.average, 20, 'should have sent 20 requests per second on average with two connections')
})
})

0 comments on commit 103fa77

Please sign in to comment.