Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unique Identifiers for POST Requests #98

Merged
merged 14 commits into from
Dec 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Available options:
Don't render the progress bar. default: false.
-l/--latency
Print all the latency data. default: false.
-I/--idReplacement
Enable replacement of [<id>] with a randomly generated ID within the request body. default: false.
-j/--json
Print the output as newline delimited json. This will cause the progress bar and results not to be rendered. default: false.
-f/--forever
Expand Down Expand Up @@ -131,7 +133,7 @@ Start autocannon against the given target.
* `bailout`: The threshold of the number of errors when making the requests to the server before this instance bail's out. This instance will take all existing results so far and aggregate them into the results. If none passed here, the instance will ignore errors and never bail out. _OPTIONAL_ default: `undefined`.
* `method`: The http method to use. _OPTIONAL_ `default: 'GET'`.
* `title`: A `String` to be added to the results for identification. _OPTIONAL_ default: `undefined`.
* `body`: A `String` or a `Buffer` containing the body of the request. Leave undefined for an empty body. _OPTIONAL_ default: `undefined`.
* `body`: A `String` or a `Buffer` containing the body of the request. Insert one or more randomly generated IDs into the body by including `[<id>]` where the randomly generated ID should be inserted. This can be useful in soak testing POST endpoints where one or more fields must be unique. Leave undefined for an empty body. _OPTIONAL_ default: `undefined`.
* `headers`: An `Object` containing the headers of the request. _OPTIONAL_ default: `{}`.
* `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_
Expand All @@ -140,6 +142,7 @@ Start autocannon against the given target.
* `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_
* `reconnectRate`: A `Number` which makes the individual connections disconnect and reconnect to the server whenever it has sent that number of requests. _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_.
* `idReplacement`: A `Boolean` which enables the replacement of `[<id>]` tags within the request body with a randomly generated ID, allowing for unique fields to be sent with requests. Check out [an example of programatic usage](./samples/using-id-replacement.js) can be found in the samples. _OPTIONAL_ default: `false`
* `forever`: A `Boolean` which allows you to setup an instance of autocannon that restarts indefinatly after emiting results with the `done` event. Useful for efficiently restarting your instance. To stop running forever, you must cause a `SIGINT` or call the `.stop()` function on your instance. _OPTIONAL_ default: `false`
* `cb`: The callback which is called on completion of a benchmark. Takes the following params. _OPTIONAL_.
* `err`: If there was an error encountered with the run.
Expand Down
6 changes: 4 additions & 2 deletions autocannon.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports.track = track

function start () {
const argv = minimist(process.argv.slice(2), {
boolean: ['json', 'n', 'help', 'renderLatencyTable', 'renderProgressBar', 'forever'],
boolean: ['json', 'n', 'help', 'renderLatencyTable', 'renderProgressBar', 'forever', 'idReplacement'],
alias: {
connections: 'c',
pipelining: 'p',
Expand All @@ -37,6 +37,7 @@ function start () {
title: 'T',
version: 'v',
forever: 'f',
idReplacement: 'I',
help: 'h'
},
default: {
Expand All @@ -49,7 +50,8 @@ function start () {
renderProgressBar: true,
json: false,
forever: false,
method: 'GET'
method: 'GET',
idReplacement: false
}
})

Expand Down
5 changes: 4 additions & 1 deletion lib/httpRequestBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ function requestBuilder (defaults) {
}

if (bodyBuf && bodyBuf.length > 0) {
headers['Content-Length'] = '' + bodyBuf.length
const idCount = reqData.idReplacement
? (bodyBuf.toString().match(/\[<id>\]/g) || []).length
: 0
headers['Content-Length'] = `${bodyBuf.length + (idCount * 27)}`
}

let req = Object.keys(headers)
Expand Down
9 changes: 7 additions & 2 deletions lib/requestIterator.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
'use strict'

const hyperid = require('hyperid')(true)
const inherits = require('util').inherits
const requestBuilder = require('./httpRequestBuilder')

Expand Down Expand Up @@ -28,9 +31,11 @@ RequestIterator.prototype.nextRequestBuffer = function () {

RequestIterator.prototype.move = function () {
// get the current buffer and proceed to next request
const ret = this.currentRequest.requestBuffer
let ret = this.currentRequest.requestBuffer
this.nextRequest()
return ret
return this.reqDefaults.idReplacement
? new Buffer(ret.toString().replace(/\[<id>\]/g, hyperid()))
: ret
}

RequestIterator.prototype.setRequests = function (newRequests) {
Expand Down
2 changes: 2 additions & 0 deletions lib/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const defaultOptions = {
amount: 0,
reconnectRate: 0,
forever: false,
idReplacement: false,
requests: [{}]
}

Expand Down Expand Up @@ -84,6 +85,7 @@ function run (opts, cb) {
url.reconnectRate = opts.reconnectRate
url.responseMax = amount || opts.maxConnectionRequests || opts.maxOverallRequests
url.rate = opts.connectionRate || opts.overallRate
url.idReplacement = opts.idReplacement

let clients = []
initialiseClients(clients)
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
],
"author": "Matteo Collina <hello@matteocollina.com>",
"contributors": [
"Glen Keane <glenkeane.94@gmail.com> (http://glenkeane.me/)"
"Glen Keane <glenkeane.94@gmail.com> (http://glenkeane.me/)",
"Donald Robertson <donaldarobertson89@gmail.com"
],
"license": "MIT",
"bugs": {
Expand All @@ -47,6 +48,7 @@
"deep-extend": "^0.4.1",
"hdr-histogram-percentiles-obj": "^1.1.0",
"http-parser-js": "^0.4.2",
"hyperid": "^1.1.0",
"minimist": "^1.2.0",
"native-hdr-histogram": "^0.4.0",
"pretty-bytes": "^3.0.1",
Expand Down
40 changes: 40 additions & 0 deletions samples/using-id-replacement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict'

const http = require('http')
const autocannon = require('autocannon')

const server = http.createServer(handle)

server.listen(0, startBench)

function handle (req, res) {
res.end('hello world')
}

function startBench () {
const url = 'http://localhost:' + server.address().port

autocannon({
url: url,
connections: 1000,
duration: 10,
requests: [
{
method: 'POST',
path: '/register',
headers: {
'Content-type': 'application/json; charset=utf-8'
},
body: JSON.stringify({
name: 'New User',
email: 'new-[<id>]@user.com' // [<id>] will be replaced with generated HyperID at run time
})
}
],
idReplacement: true
}, finishedBench)

function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
60 changes: 60 additions & 0 deletions test/httpRequestBuilder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,63 @@ test('request builder should accept all valid standard http methods', (t) => {
})
t.end()
})

test('request builder should add a Content-Length header when the body buffer exists as a default override', (t) => {
t.plan(1)

const opts = server.address()
opts.method = 'POST'
opts.body = 'body'

const build = RequestBuilder(opts)

const result = build()
t.same(result,
new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\nbody\r\n`),
'request is okay')
})

test('request builder should add a Content-Length header when the body buffer exists as per build override', (t) => {
t.plan(1)

const opts = server.address()
opts.method = 'POST'

const build = RequestBuilder(opts)

const result = build({ body: 'body' })
t.same(result,
new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\nbody\r\n`),
'request is okay')
})

test('request builder should add a Content-Length header with correct calculated value when the body buffer exists and idReplacement is enabled as a default override', (t) => {
t.plan(1)

const opts = server.address()
opts.method = 'POST'
opts.body = '[<id>]'
opts.idReplacement = true

const build = RequestBuilder(opts)

const result = build()
t.same(result,
new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 33\r\n\r\n[<id>]\r\n`),
'request is okay')
})

test('request builder should add a Content-Length header with value "[<contentLength>]" when the body buffer exists and idReplacement is enabled as a per build override', (t) => {
t.plan(1)

const opts = server.address()
opts.method = 'POST'
opts.body = '[<id>]'

const build = RequestBuilder(opts)

const result = build({ idReplacement: true })
t.same(result,
new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 33\r\n\r\n[<id>]\r\n`),
'request is okay')
})
34 changes: 34 additions & 0 deletions test/requestIterator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,37 @@ test('request iterator should allow for rebuilding the current request', (t) =>
iterator.setRequest() // this should build default request
t.same(iterator.currentRequest.requestBuffer, request5Res, 'request was okay')
})

test('request iterator should not replace any [<id>] tags with generated IDs when calling move with idReplacement disabled', (t) => {
t.plan(2)

const opts = server.address()
opts.method = 'POST'
opts.body = '[<id>]'
opts.requests = [{}]

const iterator = new RequestIterator(opts.requests, opts)
const result = iterator.move().toString().trim()

const contentLength = result.split('Content-Length: ')[1].slice(0, 1)
t.equal(contentLength, '6', 'Content-Length was incorrect')

const body = result.split('Content-Length: 6')[1].trim()
t.equal(body, '[<id>]', '[<id>] should be present in body')
})

test('request iterator should replace all [<id>] tags with generated IDs when calling move with idReplacement enabled', (t) => {
t.plan(2)

const opts = server.address()
opts.method = 'POST'
opts.body = '[<id>]'
opts.requests = [{}]
opts.idReplacement = true

const iterator = new RequestIterator(opts.requests, opts)
const result = iterator.move().toString().trim()

t.equal(result.includes('[<id>]'), false, 'One or more [<id>] tags were not replaced')
t.equal(result.slice(-1), '0', 'Generated ID should end with request number')
})