@artilleryio/int-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +46 -0
- package/LICENSE.txt +353 -0
- package/README.md +1 -0
- package/index.js +75 -0
- package/lib/engine_http.js +886 -0
- package/lib/engine_socketio.js +376 -0
- package/lib/engine_ws.js +366 -0
- package/lib/is-idle-phase.js +14 -0
- package/lib/phases.js +184 -0
- package/lib/readers.js +65 -0
- package/lib/runner.js +454 -0
- package/lib/ssms.js +715 -0
- package/lib/weighted-pick.js +88 -0
- package/package.json +49 -0
package/lib/runner.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const EventEmitter = require('eventemitter3');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const _ = require('lodash');
|
|
10
|
+
const debug = require('debug')('runner');
|
|
11
|
+
const debugPerf = require('debug')('perf');
|
|
12
|
+
const uuidv4 = require('uuid').v4;
|
|
13
|
+
const A = require('async');
|
|
14
|
+
const { SSMS } = require('./ssms');
|
|
15
|
+
const tryResolve = require('try-require').resolve;
|
|
16
|
+
const createPhaser = require('./phases');
|
|
17
|
+
const isIdlePhase = require('./is-idle-phase');
|
|
18
|
+
const createReader = require('./readers');
|
|
19
|
+
const engineUtil = require('@artilleryio/int-commons').engine_util;
|
|
20
|
+
const wl = require('./weighted-pick');
|
|
21
|
+
|
|
22
|
+
const Engines = {
|
|
23
|
+
http: require('./engine_http'),
|
|
24
|
+
ws: require('./engine_ws'),
|
|
25
|
+
socketio: require('./engine_socketio')
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
runner: runner,
|
|
30
|
+
contextFuncs: {
|
|
31
|
+
$randomString,
|
|
32
|
+
$randomNumber
|
|
33
|
+
},
|
|
34
|
+
runnerFuncs: {
|
|
35
|
+
handleScriptHook,
|
|
36
|
+
prepareScript,
|
|
37
|
+
loadProcessor
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function loadEngines(
|
|
42
|
+
script,
|
|
43
|
+
ee,
|
|
44
|
+
warnings = {
|
|
45
|
+
engines: {}
|
|
46
|
+
}
|
|
47
|
+
) {
|
|
48
|
+
const loadedEngines = _.map(
|
|
49
|
+
Object.assign({}, Engines, script.config.engines),
|
|
50
|
+
function loadEngine(engineConfig, engineName) {
|
|
51
|
+
let moduleName = 'artillery-engine-' + engineName;
|
|
52
|
+
try {
|
|
53
|
+
let Engine;
|
|
54
|
+
if (typeof Engines[engineName] !== 'undefined') {
|
|
55
|
+
Engine = Engines[engineName];
|
|
56
|
+
} else {
|
|
57
|
+
Engine = require(moduleName);
|
|
58
|
+
}
|
|
59
|
+
const engine = new Engine(script, ee, engineUtil);
|
|
60
|
+
engine.__name = engineName;
|
|
61
|
+
return engine;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.log(
|
|
64
|
+
'WARNING: engine %s specified but module %s could not be loaded',
|
|
65
|
+
engineName,
|
|
66
|
+
moduleName
|
|
67
|
+
);
|
|
68
|
+
console.log(err.stack);
|
|
69
|
+
warnings.engines[engineName] = {
|
|
70
|
+
message: 'Could not load',
|
|
71
|
+
error: err
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return { loadedEngines, warnings };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function loadProcessor(script, options) {
|
|
81
|
+
if (script.config.processor) {
|
|
82
|
+
const absoluteScriptPath = path.resolve(process.cwd(), options.scriptPath);
|
|
83
|
+
const processorPath = path.resolve(
|
|
84
|
+
path.dirname(absoluteScriptPath),
|
|
85
|
+
script.config.processor
|
|
86
|
+
);
|
|
87
|
+
const processor = require(processorPath);
|
|
88
|
+
script.config.processor = processor;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return script;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function prepareScript(script, payload) {
|
|
95
|
+
const runnableScript = _.cloneDeep(script);
|
|
96
|
+
|
|
97
|
+
_.each(runnableScript.config.phases, function (phaseSpec) {
|
|
98
|
+
phaseSpec.mode = phaseSpec.mode || runnableScript.config.mode;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (payload) {
|
|
102
|
+
if (_.isArray(payload[0])) {
|
|
103
|
+
runnableScript.config.payload = [
|
|
104
|
+
{
|
|
105
|
+
fields: runnableScript.config.payload.fields,
|
|
106
|
+
reader: createReader(runnableScript.config.payload.order, runnableScript.config.payload),
|
|
107
|
+
data: payload
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
} else {
|
|
111
|
+
runnableScript.config.payload = payload;
|
|
112
|
+
_.each(runnableScript.config.payload, function (el) {
|
|
113
|
+
el.reader = createReader(el.order, el);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
runnableScript.config.payload = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Flatten flows (can have nested arrays of request specs with YAML references):
|
|
121
|
+
_.each(runnableScript.scenarios, function (scenarioSpec) {
|
|
122
|
+
scenarioSpec.flow = _.flatten(scenarioSpec.flow);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return runnableScript;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runner(script, payload, options, callback) {
|
|
129
|
+
let opts = _.assign(
|
|
130
|
+
{
|
|
131
|
+
periodicStats: script.config.statsInterval || 30,
|
|
132
|
+
mode: script.config.mode || 'uniform'
|
|
133
|
+
},
|
|
134
|
+
options
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const metrics = new SSMS();
|
|
138
|
+
|
|
139
|
+
const warnings = {
|
|
140
|
+
engines: {}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const runnableScript = prepareScript(script, payload);
|
|
144
|
+
|
|
145
|
+
let ee = new EventEmitter();
|
|
146
|
+
|
|
147
|
+
//
|
|
148
|
+
// load engines:
|
|
149
|
+
//
|
|
150
|
+
const { loadedEngines: runnerEngines } = loadEngines(
|
|
151
|
+
runnableScript,
|
|
152
|
+
ee,
|
|
153
|
+
warnings
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const promise = new Promise(function (resolve, reject) {
|
|
157
|
+
ee.run = function (contextVars) {
|
|
158
|
+
let runState = {
|
|
159
|
+
pendingScenarios: 0,
|
|
160
|
+
// pendingRequests: 0,
|
|
161
|
+
compiledScenarios: null,
|
|
162
|
+
scenarioEvents: null,
|
|
163
|
+
picker: undefined,
|
|
164
|
+
engines: runnerEngines,
|
|
165
|
+
metrics: metrics
|
|
166
|
+
};
|
|
167
|
+
debug('run() with: %j', runnableScript);
|
|
168
|
+
run(runnableScript, ee, opts, runState, contextVars);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
ee.stop = async function (done) {
|
|
172
|
+
metrics.stop();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// FIXME: Warnings should be returned from this function instead along with
|
|
176
|
+
// the event emitter. That will be a breaking change.
|
|
177
|
+
ee.warnings = warnings;
|
|
178
|
+
|
|
179
|
+
resolve(ee);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (callback && typeof callback === 'function') {
|
|
183
|
+
promise.then(callback.bind(null, null), callback);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return promise;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function run(script, ee, options, runState, contextVars) {
|
|
190
|
+
const metrics = runState.metrics;
|
|
191
|
+
const intermediates = [];
|
|
192
|
+
|
|
193
|
+
let phaser = createPhaser(script.config.phases);
|
|
194
|
+
let scenarioContext;
|
|
195
|
+
|
|
196
|
+
phaser.on('arrival', function (spec) {
|
|
197
|
+
if (runState.pendingScenarios >= spec.maxVusers) {
|
|
198
|
+
metrics.counter('vusers.skipped', 1);
|
|
199
|
+
} else {
|
|
200
|
+
scenarioContext = runScenario(script, metrics, runState, contextVars);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
phaser.on('phaseStarted', function (spec) {
|
|
204
|
+
ee.emit('phaseStarted', spec);
|
|
205
|
+
if (isIdlePhase(spec)) {
|
|
206
|
+
ee.emit('stats', SSMS.empty());
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
phaser.on('phaseCompleted', function (spec) {
|
|
210
|
+
ee.emit('phaseCompleted', spec);
|
|
211
|
+
if (isIdlePhase(spec)) {
|
|
212
|
+
ee.emit('stats', SSMS.empty());
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
phaser.on('done', function () {
|
|
216
|
+
debug('All phases launched');
|
|
217
|
+
|
|
218
|
+
const doneYet = setInterval(function checkIfDone() {
|
|
219
|
+
if (runState.pendingScenarios === 0) {
|
|
220
|
+
clearInterval(doneYet);
|
|
221
|
+
|
|
222
|
+
metrics.aggregate(true);
|
|
223
|
+
|
|
224
|
+
const totals = SSMS.pack(intermediates);
|
|
225
|
+
|
|
226
|
+
ee.emit('done', totals);
|
|
227
|
+
} else {
|
|
228
|
+
debug('Pending scenarios: %s', runState.pendingScenarios);
|
|
229
|
+
}
|
|
230
|
+
}, 1000);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
metrics.on('metricData', (ts, periodData) => {
|
|
234
|
+
const cloned = SSMS.deserializeMetrics(SSMS.serializeMetrics(periodData));
|
|
235
|
+
intermediates.push(periodData);
|
|
236
|
+
ee.emit('stats', cloned);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
phaser.run();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function runScenario(script, metrics, runState, contextVars) {
|
|
243
|
+
const start = process.hrtime();
|
|
244
|
+
|
|
245
|
+
//
|
|
246
|
+
// Compile scenarios if needed
|
|
247
|
+
//
|
|
248
|
+
if (!runState.compiledScenarios) {
|
|
249
|
+
_.each(script.scenarios, function (scenario) {
|
|
250
|
+
if (typeof scenario.weight === 'undefined') {
|
|
251
|
+
scenario.weight = 1;
|
|
252
|
+
} else {
|
|
253
|
+
debug(`scenario ${scenario.name} weight = ${scenario.weight}`);
|
|
254
|
+
const variableValues = Object.assign(
|
|
255
|
+
datafileVariables(script),
|
|
256
|
+
inlineVariables(script),
|
|
257
|
+
{ $processEnvironment: process.env }
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const w = engineUtil.template(scenario.weight, {
|
|
261
|
+
vars: variableValues
|
|
262
|
+
});
|
|
263
|
+
scenario.weight = isNaN(parseInt(w)) ? 0 : parseInt(w);
|
|
264
|
+
debug(
|
|
265
|
+
`scenario ${scenario.name} weight has been set to ${scenario.weight}`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
runState.picker = wl(script.scenarios);
|
|
271
|
+
|
|
272
|
+
runState.scenarioEvents = new EventEmitter();
|
|
273
|
+
runState.scenarioEvents.on('counter', function (name, value) {
|
|
274
|
+
metrics.counter(name, value);
|
|
275
|
+
});
|
|
276
|
+
// TODO: Deprecate
|
|
277
|
+
runState.scenarioEvents.on('customStat', function (stat) {
|
|
278
|
+
metrics.summary(stat.stat, stat.value);
|
|
279
|
+
});
|
|
280
|
+
runState.scenarioEvents.on('summary', function (name, value) {
|
|
281
|
+
metrics.summary(name, value);
|
|
282
|
+
});
|
|
283
|
+
runState.scenarioEvents.on('histogram', function (name, value) {
|
|
284
|
+
metrics.summary(name, value);
|
|
285
|
+
});
|
|
286
|
+
runState.scenarioEvents.on('rate', function (name) {
|
|
287
|
+
metrics.rate(name);
|
|
288
|
+
});
|
|
289
|
+
runState.scenarioEvents.on('started', function () {
|
|
290
|
+
runState.pendingScenarios++;
|
|
291
|
+
});
|
|
292
|
+
// TODO: Take an object so that it can have code, description etc
|
|
293
|
+
runState.scenarioEvents.on('error', function (errCode) {
|
|
294
|
+
metrics.counter(`errors.${errCode}`, 1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
runState.compiledScenarios = _.map(
|
|
298
|
+
script.scenarios,
|
|
299
|
+
function (scenarioSpec) {
|
|
300
|
+
const name = scenarioSpec.engine || script.config.engine || 'http';
|
|
301
|
+
const engine = runState.engines.find((e) => e.__name === name);
|
|
302
|
+
return engine.createScenario(scenarioSpec, runState.scenarioEvents);
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let i = runState.picker()[0];
|
|
308
|
+
|
|
309
|
+
debug(
|
|
310
|
+
'picking scenario %s (%s) weight = %s',
|
|
311
|
+
i,
|
|
312
|
+
script.scenarios[i].name,
|
|
313
|
+
script.scenarios[i].weight
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
metrics.counter(`vusers.created_by_name.${script.scenarios[i].name || i}`, 1);
|
|
317
|
+
metrics.counter('vusers.created', 1);
|
|
318
|
+
|
|
319
|
+
const scenarioStartedAt = process.hrtime();
|
|
320
|
+
const scenarioContext = createContext(script, contextVars);
|
|
321
|
+
|
|
322
|
+
const finish = process.hrtime(start);
|
|
323
|
+
const runScenarioDelta = finish[0] * 1e9 + finish[1];
|
|
324
|
+
debugPerf(
|
|
325
|
+
'runScenarioDelta: %s',
|
|
326
|
+
Math.round((runScenarioDelta / 1e6) * 100) / 100
|
|
327
|
+
);
|
|
328
|
+
runState.compiledScenarios[i](scenarioContext, function (err, context) {
|
|
329
|
+
runState.pendingScenarios--;
|
|
330
|
+
if (err) {
|
|
331
|
+
debug(err);
|
|
332
|
+
metrics.counter('vusers.failed', 1);
|
|
333
|
+
} else {
|
|
334
|
+
metrics.counter('vusers.failed', 0);
|
|
335
|
+
metrics.counter('vusers.completed', 1);
|
|
336
|
+
const scenarioFinishedAt = process.hrtime(scenarioStartedAt);
|
|
337
|
+
const delta = scenarioFinishedAt[0] * 1e9 + scenarioFinishedAt[1];
|
|
338
|
+
metrics.summary('vusers.session_length', delta / 1e6);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return scenarioContext;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function datafileVariables(script) {
|
|
346
|
+
let result = {};
|
|
347
|
+
if (script.config.payload) {
|
|
348
|
+
_.each(script.config.payload, function (el) {
|
|
349
|
+
// If data = [] (i.e. the CSV file is empty, or only has headers and
|
|
350
|
+
// skipHeaders = true), then row could = undefined
|
|
351
|
+
let row = el.reader(el.data) || [];
|
|
352
|
+
_.each(el.fields, function (fieldName, j) {
|
|
353
|
+
result[fieldName] = row[j];
|
|
354
|
+
});
|
|
355
|
+
if (typeof el.name !== 'undefined') {
|
|
356
|
+
// Make the entire CSV available
|
|
357
|
+
result[el.name] = el.reader(el.data);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function inlineVariables(script) {
|
|
365
|
+
let result = {};
|
|
366
|
+
if (script.config.variables) {
|
|
367
|
+
_.each(script.config.variables, function (v, k) {
|
|
368
|
+
let val;
|
|
369
|
+
if (_.isArray(v)) {
|
|
370
|
+
val = _.sample(v);
|
|
371
|
+
} else {
|
|
372
|
+
val = v;
|
|
373
|
+
}
|
|
374
|
+
result[k] = val;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Create initial context for a scenario.
|
|
382
|
+
*/
|
|
383
|
+
function createContext(script, contextVars) {
|
|
384
|
+
const INITIAL_CONTEXT = {
|
|
385
|
+
vars: Object.assign(
|
|
386
|
+
{
|
|
387
|
+
target: script.config.target,
|
|
388
|
+
$environment: script._environment,
|
|
389
|
+
$processEnvironment: process.env
|
|
390
|
+
},
|
|
391
|
+
contextVars || {}
|
|
392
|
+
),
|
|
393
|
+
funcs: {
|
|
394
|
+
$randomNumber: $randomNumber,
|
|
395
|
+
$randomString: $randomString,
|
|
396
|
+
$template: (input) => engineUtil.template(input, { vars: result.vars })
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
let result = INITIAL_CONTEXT;
|
|
401
|
+
|
|
402
|
+
// variables from payloads:
|
|
403
|
+
const variableValues1 = datafileVariables(script);
|
|
404
|
+
Object.assign(result.vars, variableValues1);
|
|
405
|
+
// inline variables:
|
|
406
|
+
const variableValues2 = inlineVariables(script);
|
|
407
|
+
Object.assign(result.vars, variableValues2);
|
|
408
|
+
|
|
409
|
+
result._uid = uuidv4();
|
|
410
|
+
result.vars.$uuid = result._uid;
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
//
|
|
415
|
+
// Generator functions for template strings:
|
|
416
|
+
//
|
|
417
|
+
function $randomNumber(min, max) {
|
|
418
|
+
return _.random(min, max);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function $randomString(length) {
|
|
422
|
+
return Math.random().toString(36).substr(2, length);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function handleScriptHook(hook, script, hookEvents, contextVars = {}) {
|
|
426
|
+
if (!script[hook]) {
|
|
427
|
+
return {};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const { loadedEngines: engines } = loadEngines(script, hookEvents);
|
|
431
|
+
const ee = new EventEmitter();
|
|
432
|
+
|
|
433
|
+
return new Promise(function (resolve, reject) {
|
|
434
|
+
ee.on('request', function () {
|
|
435
|
+
hookEvents.emit(`${hook}TestRequest`);
|
|
436
|
+
});
|
|
437
|
+
ee.on('error', function (error) {
|
|
438
|
+
hookEvents.emit(`${hook}TestError`, error);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const name = script[hook].engine || 'http';
|
|
442
|
+
const engine = engines.find((e) => e.__name === name);
|
|
443
|
+
const hookScenario = engine.createScenario(script[hook], ee);
|
|
444
|
+
const hookContext = createContext(script, contextVars);
|
|
445
|
+
hookScenario(hookContext, function (err, context) {
|
|
446
|
+
if (err) {
|
|
447
|
+
debug(err);
|
|
448
|
+
return reject(err);
|
|
449
|
+
} else {
|
|
450
|
+
return resolve(context.vars);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|