master
Raw Download raw file
  1#!/usr/bin/env node
  2
  3"use strict";
  4
  5var assert = require('assert');
  6var events = require('events');
  7var fs = require('fs');
  8var http = require('http');
  9var nopt = require('nopt');
 10var path = require('path');
 11var seedModule = require('seed-random');
 12var underscore = require('underscore');
 13
 14var checkServer = require('./lib/check_server');
 15var util = require('./lib/util');
 16
 17var Request = function (client, roundIndex, nonce) {
 18  this.client = client;
 19  this.simulation = client.simulation;
 20  this.roundIndex = roundIndex;
 21  this.nonce = nonce;
 22};
 23
 24Request.prototype.log = function (message, force) {
 25  if (this.client.simulation.debugOutput || force) {
 26    console.log('Request[%s,%s]: %s',
 27      this.client.ip.slice(0, 8),
 28      this.nonce.slice(0, 8),
 29      message);
 30  }
 31};
 32
 33Request.prototype.handleResponse = function (res, pageData) {
 34  var jsonResponse,
 35      hmac;
 36  if (res.statusCode !== 200) {
 37    this.log("Non-200 return code.");
 38    return;
 39  }
 40  try {
 41    jsonResponse = JSON.parse(pageData);
 42  } catch (err) {
 43    this.log("Could not parse response body.", true);
 44    return;
 45  }
 46  if (!jsonResponse.hmac) {
 47    this.log("Responded with no HMAC.", true);
 48    return;
 49  }
 50  hmac = util.sign(this.nonce, this.client.simulation.secret);
 51  if (hmac === jsonResponse.hmac) {
 52    this.simulation.registerSuccess(this.client, this.roundIndex, this.latency);
 53  } else {
 54    this.log("Responded with invalid HMAC", true);
 55  }
 56};
 57
 58Request.prototype.run = function () {
 59  var self = this;
 60  var baseOptions = {
 61    path: '/?nonce=' + self.nonce,
 62    headers: {
 63      'X-Forwarded-For': self.client.ip
 64    }
 65  };
 66  var options = underscore.extend(self.client.simulation.connection, baseOptions);
 67  var startTime = Date.now();
 68  var req = http.get(options, function (res) {
 69    var pageData = "";
 70    res.on('data', function (chunk) {
 71      pageData += chunk;
 72    });
 73
 74    res.on('end', function () {
 75      self.latency = Date.now() - startTime;
 76      self.handleResponse(res, pageData);
 77    });
 78  });
 79
 80  req.on('socket', function (socket) {
 81    socket.setMaxListeners(0)
 82    socket.setTimeout(1000); // In ms
 83    socket.on('timeout', function () {
 84      self.log("Timeout. Aborting.");
 85      req.abort();
 86    });
 87  });
 88
 89  req.on("error", function (e) {
 90    // Socket errors are expected when we our resources are small compared
 91    // to the traffic that we try to push through.
 92    self.log("Sword HTTP error: " + e.message);
 93  });
 94};
 95
 96var Client = function (simulation, birthRound) {
 97  this.simulation = simulation;
 98  this.ip = simulation.randString(48);
 99  simulation.clients[this.ip] = this;
100  this.birthRound = birthRound;
101  this.lifetime = simulation.simulationParameters.clientLifetime;
102  if (simulation.random() > simulation.simulationParameters.pElephant) {
103    this.type = "mouse";
104    this.delayUntilStart = simulation.random() * simulation.simulationParameters.roundLength;
105    this.requestsPerRound = simulation.simulationParameters.mouseRequestsPerRound;
106  } else {
107    this.type = "elephant";
108    this.delayUntilStart = (simulation.random() / 5) * simulation.simulationParameters.roundLength;
109    this.requestsPerRound = simulation.simulationParameters.elephantRequestsPerRound;
110  }
111};
112
113Client.prototype.expired = function (roundIndex) {
114  return (this.lifetime <= roundIndex - this.birthRound);
115};
116
117Client.prototype.waitTime = function () {
118  return this.simulation.simulationParameters.roundLength / this.requestsPerRound;
119};
120
121Client.prototype.runMouse = function (roundIndex) {
122  var roundLength = this.simulation.simulationParameters.roundLength,
123      waitBetweenRequests = (roundLength - this.delayUntilStart) / this.requestsPerRound;
124  setTimeout(
125    this.sendRequest.bind(this),
126    this.delayUntilStart,
127    0,
128    roundIndex,
129    waitBetweenRequests
130  );
131};
132
133Client.prototype.runElephant = function (roundIndex) {
134  var waitBetweenRequests = this.simulation.simulationParameters.roundLength / this.requestsPerRound;
135  this.sendRequest(0, roundIndex, waitBetweenRequests);
136};
137
138Client.prototype.run = function (roundIndex) {
139  switch (this.type) {
140  case "mouse":
141    this.runMouse(roundIndex);
142    break;
143  case "elephant":
144    this.runElephant(roundIndex);
145    break;
146  default:
147    assert(false, "Fell through cases.");
148  }
149};
150
151Client.prototype.sendRequest = function (requestIndex, roundIndex, waitBetweenRequests) {
152  var requestNonce = util.generateRandom(64);
153  var request = new Request(this, roundIndex, requestNonce);
154  request.run();
155  var nextRequestIndex = requestIndex + 1;
156  if (nextRequestIndex < this.requestsPerRound) {
157    setTimeout(
158      this.sendRequest.bind(this),
159      waitBetweenRequests,
160      nextRequestIndex,
161      roundIndex,
162      waitBetweenRequests
163    );
164  }
165};
166
167var Simulation = function (seed, simulationParameters) {
168  this.simulationParameters = simulationParameters;
169  this.random = seedModule(seed);
170  this.simulationResults = [];
171  this.responseTotal = 0;
172  this.clients = {};
173  this.currentRound = -1;
174  this.roundStartTimes = [];
175};
176
177Simulation.prototype.randString = function (length) {
178  return util.randString(this.random, length);
179};
180
181Simulation.prototype.duration = function () {
182  return this.simulationParameters['roundLength'] * this.simulationParameters['roundCount'];
183};
184
185Simulation.prototype.runSimulation = function (clients) {
186  if (clients === undefined) {
187    clients = [];
188  }
189  var roundIndex = ++this.currentRound;
190  var i;
191  var client;
192
193  this.roundStartTimes[roundIndex] = Date.now();
194  this.simulationResults[roundIndex] = [];
195
196  var runningClients = [];
197  // Create clients to replace any that have expired
198  for (i = 0; i < clients.length; i++) {
199    client = clients[i];
200    if (!client.expired(roundIndex)) {
201      runningClients.push(client);
202    }
203  }
204  var clientsToCreate = this.simulationParameters.clientsPerRound - runningClients.length;
205  for (i = 0; i < clientsToCreate; i++) {
206    client = new Client(this, roundIndex);
207    runningClients.push(client);
208  }
209  // Run the clients
210  for (i = 0; i < runningClients.length; i++) {
211    runningClients[i].run(roundIndex);
212  }
213  if (roundIndex + 1 >= this.simulationParameters.roundCount) {
214    setTimeout(
215      this.scoreSimulation.bind(this),
216      this.simulationParameters.roundLength
217    );
218  } else {
219    setTimeout(
220      this.runSimulation.bind(this),
221      this.simulationParameters.roundLength,
222      runningClients
223    );
224  }
225};
226
227Simulation.prototype.registerSuccess = function (client, roundIndex, latency) {
228  this.responseTotal += 1;
229  if (client.type === "elephant") {
230    // No points for elephants!
231    return;
232  }
233  var mouseCount = this.simulationResults[roundIndex][client.ip] || 0;
234  this.simulationResults[roundIndex][client.ip] = mouseCount + 1;
235  this.maxLatency = this.maxLatency ? Math.max(this.maxLatency, latency) : latency;
236};
237
238Simulation.prototype.scoreRound = function (roundResults) {
239  console.log("Round");
240  var score = 0;
241  var self = this;
242  Object.keys(roundResults).forEach(function (ip) {
243    var successes = roundResults[ip];
244    var client = self.clients[ip];
245    console.log("IP %s (%s): %d", ip.slice(0, 8), client.type, successes);
246    score += successes;
247  });
248  if (score == 0) {
249    console.log("No mice got through this round.");
250  }
251  return score;
252};
253
254Simulation.prototype.calculateDowntime = function (responseCount) {
255  // Calculate the number of additional requests that could have been handled
256  var totalTime = this.simulationParameters['backendCount'] * this.simulationParameters['backendInFlight'] * this.duration(),
257      potentialResponses = totalTime / this.simulationParameters['backendProcessingTime'];
258  return potentialResponses - responseCount;
259}
260
261Simulation.prototype.scoreSimulation = function () {
262  console.log("Scoring now");
263  var resultsPath = this.resultsPath,
264      roundScores = this.simulationResults.map(this.scoreRound.bind(this)),
265      goodCount = util.sum(roundScores),
266      backendDeficit = this.calculateDowntime(this.responseTotal);
267
268  var output = {
269    "good_responses": goodCount,
270    "backend_deficit": backendDeficit,
271    "correct": true
272  };
273  console.log("Number of total responses %s", this.responseTotal)
274  console.log("Number of good responses: %s", goodCount);
275  console.log("Number of responses less than ideal: %s", backendDeficit);
276  fs.writeFileSync(resultsPath, JSON.stringify(output));
277  process.exit();
278};
279
280function main() {
281  var opts = {
282    "secret": String,
283    "out-socket": String,
284    "out-port": String,
285    "check-server": Boolean,
286    "debug": Boolean,
287    "results-path": String,
288    "run-time": Number
289  };
290  var parsed = nopt(opts),
291      resultsPath = parsed['results-path'] || path.resolve(__dirname, "results.json"),
292      secret = parsed.secret || "defaultsecret",
293      secretHash = util.hash(secret),
294      connectionOptions,
295      seed,
296      simulationParameters;
297
298  if (parsed['out-socket'] !== undefined && parsed['out-port'] !== undefined) {
299    console.log("Cannot specify both an out-port and an out-socket. Exiting.");
300    process.exit(1);
301  } else if (parsed['out-socket']) {
302    connectionOptions = {'socketPath': parsed['out-socket']};
303  } else {
304    connectionOptions = {
305      'host': 'localhost',
306      'port':  parsed['out-port'] || '3000'
307    };
308  }
309  connectionOptions['path'] = "/" + secretHash;
310
311  if (parsed.argv.remain.length > 1) {
312    console.log("Expected at most one extra arg and received more. Exiting.");
313  }
314  seed = parsed.argv.remain[0] || util.generateRandom(32);
315  console.log("Using seed %s", seed);
316
317  simulationParameters = {
318    'clientLifetime': 2, // In rounds
319    'roundLength': 500, // In ms
320    'roundCount': 40,
321    'clientsPerRound': 5,
322    'pElephant': 0.4,
323    'mouseRequestsPerRound': 2,
324    'elephantRequestsPerRound': 50,
325    'backendCount': 2,
326    'backendInFlight': 2,
327    'backendProcessingTime': 75
328  };
329
330  if (parsed['run-time']) {
331    simulationParameters.roundCount = Math.floor(
332      parsed['run-time'] * 1000 / simulationParameters.roundLength);
333  }
334
335  var simulation = new Simulation(seed, simulationParameters);
336  simulation.connection = connectionOptions;
337  simulation.resultsPath = resultsPath;
338  simulation.debugOutput = parsed['debug'];
339  simulation.secret = secret;
340
341  checkServer.checkWithBackoff(
342    connectionOptions,
343    function () {
344      if (parsed['check-server']) {
345        // Just check that the server is up.
346        console.log("Server is up.");
347        process.exit(0);
348      } else {
349        simulation.runSimulation();
350      }
351    },
352    function () {
353      console.log("Server is not up. Exiting.");
354      process.exit(1);
355    }
356  );
357}
358
359main();