Last active
November 19, 2015 19:27
-
-
Save jakerella/c929d26253c6799b68a3 to your computer and use it in GitHub Desktop.
Simple load generation script to test web applications.
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 script helps to artifically generate load on a web application through | |
* weighted requests to various endpoints. It may not be pretty, but it works | |
* for me. :) Feel free to use however you want. | |
* | |
* NOTE: Please use responsibly, don't run this script against a production server! | |
* | |
* @author Jordan Kasper (@jakerella) | |
* @contributor @robcolburn | |
* @license MIT | |
*/ | |
var http = require('http'); | |
var config = { | |
// Options passed directly into the http.request(...) method on each request | |
httpOptions: { | |
host: 'localhost', | |
port: 3000, | |
headers: { | |
'Content-Type': 'application/json' | |
} | |
}, | |
// Use the array below to specify requests to send to the application | |
// Note that the "weights" should add up to 1.0, but will be approximated | |
requests: [ | |
{ path: '/', method: 'GET', weight: 0.1 }, | |
{ path: '/api/CoffeeShops', method: 'GET', weight: 0.5 }, | |
{ path: '/api/CoffeeShops/1', method: 'GET', weight: 0.1 }, | |
'/api/CoffeeShop/2', | |
//{ path: '/api/CoffeeShops', method: 'POST', weight: 0.2, data: JSON.stringify({ | |
// name: "foo-" + (new Date()).getTime() | |
//})}, | |
{ path: '/api/Reviews', method: 'GET', weight: 0.2 } | |
], | |
responseEncoding: 'utf8', | |
stopOnReqError: true, // If true, stops the Node process on request errors (NOT http status codes) | |
logHTTPErrors: true, // Logs any 400+ status code... can be chatty | |
stopOn400: false, // Forces an exit on the Node process on 4XX errors | |
stopOn500: true, // Forces an exit on the Node process on 5XX errors | |
requestsPerSecond: 20, | |
trafficSpikeFreq: [5000, 10000], // every X to Y seconds (set to null to turn off) (can also be a flat number) | |
trafficSpikeAmplitude: [100, 200], // make X to Y requests each spike (can also be a flat number) | |
// The items below are used internally and may be overridden, don't use them! | |
weighted: [], | |
waitTime: 1000 | |
}; | |
(function setup() { | |
var spikeTimeout = getSpikeTimeout(), | |
maxWeight = (config.requests.length > 10 && 100) || 10; | |
// Lazy. Allow requests to just be paths. | |
config.requests = config.requests.map(function(req){ | |
return typeof(req) === 'string' ? {path: req} : req; | |
}); | |
config.requests.forEach(function(request, reqIndex) { | |
for (var i=0; i < (request.weight || 0.1) * maxWeight; ++i) { | |
config.weighted.push(reqIndex); | |
} | |
}); | |
config.waitTime = 1000 / config.requestsPerSecond | |
console.log('Beginning load generation at 1 req per ' + config.waitTime + 'ms.'); | |
console.log('Using weighted requests:\n' + config.weighted); | |
console.log('Press "Ctrl + C" to stop.\n'); | |
sendNext(); | |
if (spikeTimeout) { | |
setTimeout(sendSpike, spikeTimeout); | |
} | |
})(); | |
function sendNext() { | |
var nextIndex = config.weighted[ Math.floor(Math.random() * config.weighted.length) ], | |
request = config.requests[ nextIndex ]; | |
sendRequest(request.method, request.path, request.data, function(err, res) { | |
if (err) { | |
console.error(err); | |
} | |
}); | |
setTimeout(sendNext, config.waitTime); | |
} | |
function sendSpike() { | |
var i, nextIndex, request, | |
numRequests = 0, | |
nextSpike = getSpikeTimeout(); | |
if (typeof config.trafficSpikeAmplitude === 'number') { | |
numRequests = config.trafficSpikeAmplitude; | |
} else { | |
numRequests = Math.floor( | |
(Math.random() * (config.trafficSpikeAmplitude[1] - config.trafficSpikeAmplitude[0])) + | |
config.trafficSpikeAmplitude[0] | |
); | |
} | |
console.log('Sending traffic spike with ' + numRequests + ' requests...'); | |
for (i=0; i<numRequests; ++i) { | |
nextIndex = config.weighted[ Math.floor(Math.random() * config.weighted.length) ]; | |
request = config.requests[ nextIndex ]; | |
sendRequest(request.method, request.path, request.data); | |
} | |
if (nextSpike) { | |
setTimeout(sendSpike, nextSpike); | |
} | |
} | |
function sendRequest(method, path, data, cb) { | |
var options = config.httpOptions || {}; | |
options.path = path || '/'; | |
options.method = method || 'GET'; | |
options.headers = options.headers || {}; | |
options.headers['Content-Length'] = (data && data.length) || 0; | |
var req = http.request(options, function(res) { | |
var err = null, | |
body = ''; | |
res.setEncoding(config.responseEncoding); | |
res.on('data', function (chunk) { | |
body += chunk; | |
}); | |
res.on('end', function () { | |
res.body = body; | |
if (res.statusCode > 399) { | |
err = new Error(body); | |
err.status = err.code = res.statusCode; | |
if (config.logHTTPErrors) { | |
console.error('Server returned error from ', options.method + ' ' + options.path, res.statusCode); | |
} | |
if (res.statusCode > 499 && config.stopOn500) { | |
process.exit(1); | |
} else if (res.statusCode > 399 && config.stopOn400) { | |
process.exit(1); | |
} | |
} | |
cb && cb(err, res); | |
}); | |
}); | |
req.on('error', function(err) { | |
console.error('Error with request:', err.message); | |
if (config.stopOnReqError) { | |
process.exit(1); | |
} else { | |
cb && cb(err); | |
} | |
}); | |
if (data) { | |
req.write(data); | |
} | |
req.end(); | |
} | |
function getSpikeTimeout() { | |
if (typeof config.trafficSpikeFreq === 'number') { | |
return config.trafficSpikeFreq; | |
} else if (config.trafficSpikeFreq && config.trafficSpikeFreq.splice && config.trafficSpikeFreq.length === 2) { | |
return Math.floor( | |
(Math.random() * (config.trafficSpikeFreq[1] - config.trafficSpikeFreq[0])) + | |
config.trafficSpikeFreq[0] | |
); | |
} else { | |
return null; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment