Skip to content

Instantly share code, notes, and snippets.

@jakerella
Last active November 19, 2015 19:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jakerella/c929d26253c6799b68a3 to your computer and use it in GitHub Desktop.
Save jakerella/c929d26253c6799b68a3 to your computer and use it in GitHub Desktop.
Simple load generation script to test web applications.
/**
* 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