master
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();