@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/engine_ws.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
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 async = require('async');
|
|
8
|
+
const _ = require('lodash');
|
|
9
|
+
const WebSocket = require('ws');
|
|
10
|
+
const HttpsProxyAgent = require('https-proxy-agent');
|
|
11
|
+
const debug = require('debug')('ws');
|
|
12
|
+
const url = require('url');
|
|
13
|
+
const engineUtil = require('@artilleryio/int-commons').engine_util;
|
|
14
|
+
const template = engineUtil.template;
|
|
15
|
+
|
|
16
|
+
module.exports = WSEngine;
|
|
17
|
+
|
|
18
|
+
function WSEngine(script) {
|
|
19
|
+
this.config = script.config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
WSEngine.prototype.createScenario = function (scenarioSpec, ee) {
|
|
23
|
+
const self = this;
|
|
24
|
+
const tasks = _.map(scenarioSpec.flow, function (rs) {
|
|
25
|
+
if (typeof rs.think !== 'undefined') {
|
|
26
|
+
return engineUtil.createThink(
|
|
27
|
+
rs,
|
|
28
|
+
_.get(self.config, 'defaults.think', {})
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return self.step(rs, ee);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return self.compile(tasks, scenarioSpec.flow, ee);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function getMessageHandler(context, params, ee, timeout, callback) {
|
|
39
|
+
let done = false;
|
|
40
|
+
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
if (!done) {
|
|
43
|
+
const err = 'response timeout';
|
|
44
|
+
ee.emit('error', err);
|
|
45
|
+
return callback(err, context);
|
|
46
|
+
}
|
|
47
|
+
}, timeout * 1000);
|
|
48
|
+
|
|
49
|
+
return function messageHandler(event) {
|
|
50
|
+
ee.emit('counter', 'websocket.messages_received', 1);
|
|
51
|
+
ee.emit('rate', 'websocket.receive_rate');
|
|
52
|
+
done = true;
|
|
53
|
+
const { data } = event;
|
|
54
|
+
|
|
55
|
+
debug('WS receive: %s', data);
|
|
56
|
+
|
|
57
|
+
if (!data) {
|
|
58
|
+
return callback(new Error('Empty response from WS server'), context);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let fauxResponse;
|
|
62
|
+
try {
|
|
63
|
+
fauxResponse = { body: JSON.parse(data) };
|
|
64
|
+
} catch (err) {
|
|
65
|
+
fauxResponse = { body: event.data };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
engineUtil.captureOrMatch(
|
|
69
|
+
params,
|
|
70
|
+
fauxResponse,
|
|
71
|
+
context,
|
|
72
|
+
function captured(err, result) {
|
|
73
|
+
if (err) {
|
|
74
|
+
ee.emit('error', err.message || err.code);
|
|
75
|
+
return callback(err, context);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { captures = {}, matches = {} } = result;
|
|
79
|
+
|
|
80
|
+
debug('matches: ', matches);
|
|
81
|
+
debug('captures: ', captures);
|
|
82
|
+
|
|
83
|
+
// match and capture are strict by default:
|
|
84
|
+
const haveFailedMatches = _.some(result.matches, function (v) {
|
|
85
|
+
return !v.success && v.strict !== false;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const haveFailedCaptures = _.some(result.captures, function (v) {
|
|
89
|
+
return v.failed;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (haveFailedMatches || haveFailedCaptures) {
|
|
93
|
+
// TODO: Emit the details of each failed capture/match
|
|
94
|
+
return callback(new Error('Failed capture or match'), context);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_.each(result.matches, function (v) {
|
|
98
|
+
ee.emit('match', v.success, {
|
|
99
|
+
expected: v.expected,
|
|
100
|
+
got: v.got,
|
|
101
|
+
expression: v.expression,
|
|
102
|
+
strict: v.strict
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
_.each(result.captures, function (v, k) {
|
|
107
|
+
_.set(context.vars, k, v.value);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return callback(null, context);
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
WSEngine.prototype.step = function (requestSpec, ee) {
|
|
117
|
+
const self = this;
|
|
118
|
+
|
|
119
|
+
if (requestSpec.loop) {
|
|
120
|
+
const steps = _.map(requestSpec.loop, function (rs) {
|
|
121
|
+
return self.step(rs, ee);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return engineUtil.createLoopWithCount(requestSpec.count || -1, steps, {
|
|
125
|
+
loopValue: requestSpec.loopValue || '$loopCount',
|
|
126
|
+
overValues: requestSpec.over,
|
|
127
|
+
whileTrue: self.config.processor
|
|
128
|
+
? self.config.processor[requestSpec.whileTrue]
|
|
129
|
+
: undefined
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (requestSpec.think) {
|
|
134
|
+
return engineUtil.createThink(
|
|
135
|
+
requestSpec,
|
|
136
|
+
_.get(self.config, 'defaults.think', {})
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (requestSpec.function) {
|
|
141
|
+
return function (context, callback) {
|
|
142
|
+
const processFunc = self.config.processor[requestSpec.function];
|
|
143
|
+
if (processFunc) {
|
|
144
|
+
processFunc(context, ee, function () {
|
|
145
|
+
return callback(null, context);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (requestSpec.log) {
|
|
152
|
+
return function (context, callback) {
|
|
153
|
+
console.log(template(requestSpec.log, context));
|
|
154
|
+
return process.nextTick(function () {
|
|
155
|
+
callback(null, context);
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (requestSpec.connect) {
|
|
161
|
+
return function (context, callback) {
|
|
162
|
+
return process.nextTick(function () {
|
|
163
|
+
callback(null, context);
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const f = function (context, callback) {
|
|
169
|
+
const params = requestSpec.wait || requestSpec.send;
|
|
170
|
+
|
|
171
|
+
// match exists on a string, so check match is not a prototype
|
|
172
|
+
let captureOrMatch = _.has(params, 'capture') || _.has(params, 'match');
|
|
173
|
+
|
|
174
|
+
if (captureOrMatch) {
|
|
175
|
+
// only process response if we're capturing
|
|
176
|
+
let timeout =
|
|
177
|
+
self.config.timeout || _.get(self.config, 'ws.timeout') || 10;
|
|
178
|
+
context.ws.onmessage = getMessageHandler(
|
|
179
|
+
context,
|
|
180
|
+
params,
|
|
181
|
+
ee,
|
|
182
|
+
timeout,
|
|
183
|
+
callback
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
// Reset onmessage to stop steps interfering with each other
|
|
187
|
+
context.ws.onmessage = undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Backwards compatible with previous version of `send` api
|
|
191
|
+
let payload = captureOrMatch ? params.payload : params;
|
|
192
|
+
|
|
193
|
+
if (payload !== undefined) {
|
|
194
|
+
payload = template(payload, context);
|
|
195
|
+
if (typeof payload === 'object') {
|
|
196
|
+
payload = JSON.stringify(payload);
|
|
197
|
+
} else {
|
|
198
|
+
payload = _.toString(payload);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ee.emit('counter', 'websocket.messages_sent', 1);
|
|
202
|
+
ee.emit('rate', 'websocket.send_rate');
|
|
203
|
+
debug('WS send: %s', payload);
|
|
204
|
+
|
|
205
|
+
context.ws.send(payload, function (err) {
|
|
206
|
+
if (err) {
|
|
207
|
+
debug(err);
|
|
208
|
+
ee.emit('error', err);
|
|
209
|
+
return callback(err, null);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// End step if we're not capturing
|
|
213
|
+
if (!captureOrMatch) {
|
|
214
|
+
return callback(null, context);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
} else if (captureOrMatch) {
|
|
218
|
+
debug('WS wait: %j', params);
|
|
219
|
+
} else {
|
|
220
|
+
// in the end, we could not send anything, so report it and stop
|
|
221
|
+
let err = 'invalid_step';
|
|
222
|
+
debug(err, requestSpec);
|
|
223
|
+
ee.emit('error', err);
|
|
224
|
+
return callback(err, context);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
return f;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
function getWsOptions(config) {
|
|
232
|
+
const options = getWsConfig(config);
|
|
233
|
+
const subprotocols = _.get(config, 'ws.subprotocols', []);
|
|
234
|
+
const headers = _.get(config, 'ws.headers', {});
|
|
235
|
+
|
|
236
|
+
const subprotocolHeader = _.find(headers, (value, headerName) => {
|
|
237
|
+
return headerName.toLowerCase() === 'sec-websocket-protocol';
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (typeof subprotocolHeader !== 'undefined') {
|
|
241
|
+
// NOTE: subprotocols defined via config.ws.subprotocols take precedence:
|
|
242
|
+
subprotocols.push(...subprotocolHeader.split(',').map((s) => s.trim()));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { options, subprotocols };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getWsInstance(config, scenarioSpec, context, cb) {
|
|
249
|
+
let wsArgs = {
|
|
250
|
+
...getWsOptions(config),
|
|
251
|
+
target: config.target
|
|
252
|
+
};
|
|
253
|
+
const [{ connect }] = scenarioSpec;
|
|
254
|
+
|
|
255
|
+
if (connect) {
|
|
256
|
+
if (connect.function && config.processor[connect.function]) {
|
|
257
|
+
const processFn = config.processor[connect.function];
|
|
258
|
+
|
|
259
|
+
return processFn(wsArgs, context, (err) => {
|
|
260
|
+
if (err) {
|
|
261
|
+
debug('connect.function', err);
|
|
262
|
+
return cb(err, null);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
context.wsArgs = wsArgs;
|
|
266
|
+
|
|
267
|
+
return cb(null, context);
|
|
268
|
+
});
|
|
269
|
+
} else if (_.isPlainObject(connect)) {
|
|
270
|
+
const {
|
|
271
|
+
target = config.target,
|
|
272
|
+
headers = _.get(config, 'ws.headers', {}),
|
|
273
|
+
subprotocols = _.get(config, 'ws.subprotocols', []),
|
|
274
|
+
...instanceConfig
|
|
275
|
+
} = connect;
|
|
276
|
+
|
|
277
|
+
const opt = getWsOptions({
|
|
278
|
+
tls: config.tls,
|
|
279
|
+
ws: { subprotocols, headers, ...instanceConfig }
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
wsArgs = {
|
|
283
|
+
target: template(target, context),
|
|
284
|
+
...opt
|
|
285
|
+
};
|
|
286
|
+
} else {
|
|
287
|
+
wsArgs.target = template(connect, context);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
debug('new WebSocket instance:', wsArgs);
|
|
292
|
+
|
|
293
|
+
context.wsArgs = wsArgs;
|
|
294
|
+
|
|
295
|
+
return cb(null, context);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
WSEngine.prototype.compile = function (tasks, scenarioSpec, ee) {
|
|
299
|
+
const config = this.config;
|
|
300
|
+
|
|
301
|
+
return function scenario(initialContext, callback) {
|
|
302
|
+
function zero(cb) {
|
|
303
|
+
ee.emit('started');
|
|
304
|
+
|
|
305
|
+
getWsInstance(config, scenarioSpec, initialContext, cb);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function one(context, cb) {
|
|
309
|
+
const { wsArgs, ...contextWithoutWsArgs } = context;
|
|
310
|
+
const ws = new WebSocket(
|
|
311
|
+
wsArgs.target,
|
|
312
|
+
wsArgs.subprotocols,
|
|
313
|
+
wsArgs.options
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
ws.on('open', function () {
|
|
317
|
+
contextWithoutWsArgs.ws = ws;
|
|
318
|
+
|
|
319
|
+
return cb(null, contextWithoutWsArgs);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
ws.once('error', function (err) {
|
|
323
|
+
debug(err);
|
|
324
|
+
ee.emit('error', err.message || err.code);
|
|
325
|
+
|
|
326
|
+
return cb(err, {});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
initialContext._successCount = 0;
|
|
331
|
+
|
|
332
|
+
const steps = _.flatten([zero, one, tasks]);
|
|
333
|
+
|
|
334
|
+
async.waterfall(steps, function scenarioWaterfallCb(err, context) {
|
|
335
|
+
if (err) {
|
|
336
|
+
debug(err);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (context && context.ws) {
|
|
340
|
+
context.ws.close();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return callback(err, context);
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
function getWsConfig(config) {
|
|
349
|
+
const tls = config.tls || {};
|
|
350
|
+
const { proxy, ...options } = config.ws || {};
|
|
351
|
+
|
|
352
|
+
if (proxy) {
|
|
353
|
+
const { url: proxyUrl, ...proxyOptions } = proxy;
|
|
354
|
+
|
|
355
|
+
debug('Set proxy: %s, options: %s', proxyUrl, proxyOptions);
|
|
356
|
+
|
|
357
|
+
const agent = new HttpsProxyAgent({
|
|
358
|
+
...url.parse(proxyUrl),
|
|
359
|
+
...proxyOptions
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
options.agent = agent;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return _.extend(tls, options);
|
|
366
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
function isIdlePhase(phase) {
|
|
6
|
+
return (
|
|
7
|
+
(phase.arrivalRate === 0 && !phase.rampTo) ||
|
|
8
|
+
phase.arrivalCount === 0 ||
|
|
9
|
+
phase.maxVusers === 0 ||
|
|
10
|
+
phase.pause > 0
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = isIdlePhase;
|
package/lib/phases.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
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 async = require('async');
|
|
9
|
+
const _ = require('lodash');
|
|
10
|
+
const isUndefined = _.isUndefined;
|
|
11
|
+
const arrivals = require('arrivals');
|
|
12
|
+
const debug = require('debug')('phases');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const driftless = require('driftless');
|
|
15
|
+
|
|
16
|
+
module.exports = phaser;
|
|
17
|
+
|
|
18
|
+
function phaser(phaseSpecs) {
|
|
19
|
+
let ee = new EventEmitter();
|
|
20
|
+
|
|
21
|
+
let tasks = _.map(phaseSpecs, function (spec, i) {
|
|
22
|
+
// Cast defined but non-number (eg: from ENV) values
|
|
23
|
+
[
|
|
24
|
+
'arrivalRate',
|
|
25
|
+
'arrivalCount',
|
|
26
|
+
'pause',
|
|
27
|
+
'rampTo',
|
|
28
|
+
'duration',
|
|
29
|
+
'maxVusers'
|
|
30
|
+
].forEach(function (k) {
|
|
31
|
+
if (!isUndefined(spec[k]) && typeof spec[k] !== 'number') {
|
|
32
|
+
spec[k] = _.toNumber(spec[k]);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (isUndefined(spec.index)) {
|
|
37
|
+
spec.index = i;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!isUndefined(spec.arrivalRate) && isUndefined(spec.rampTo)) {
|
|
41
|
+
spec.mode = spec.mode || 'uniform';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!isUndefined(spec.pause)) {
|
|
45
|
+
return createPause(spec, ee);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!isUndefined(spec.arrivalCount)) {
|
|
49
|
+
return createArrivalCount(spec, ee);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!isUndefined(spec.arrivalRate)) {
|
|
53
|
+
// If arrivalRate is zero and it's not a ramp, it's the same as a pause:
|
|
54
|
+
if (spec.arrivalRate === 0 && isUndefined(spec.rampTo)) {
|
|
55
|
+
return createPause(Object.assign(spec, { pause: spec.duration }), ee);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// If it's a ramp, create that:
|
|
59
|
+
if (!isUndefined(spec.rampTo)) {
|
|
60
|
+
return createRamp(spec, ee);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Otherwise it's a plain arrival phase:
|
|
64
|
+
return createArrivalRate(spec, ee);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('Unknown phase spec\n%j\nThis should not happen', spec);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
ee.run = function () {
|
|
71
|
+
async.series(tasks, function (err) {
|
|
72
|
+
if (err) {
|
|
73
|
+
debug(err);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ee.emit('done');
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return ee;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createPause(spec, ee) {
|
|
84
|
+
const duration = spec.pause * 1000;
|
|
85
|
+
const task = function (callback) {
|
|
86
|
+
ee.emit('phaseStarted', spec);
|
|
87
|
+
setTimeout(function () {
|
|
88
|
+
ee.emit('phaseCompleted', spec);
|
|
89
|
+
return callback(null);
|
|
90
|
+
}, duration);
|
|
91
|
+
};
|
|
92
|
+
return task;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createRamp(spec, ee) {
|
|
96
|
+
const duration = spec.duration || 1;
|
|
97
|
+
const arrivalRate = spec.arrivalRate;
|
|
98
|
+
const rampTo = spec.rampTo;
|
|
99
|
+
|
|
100
|
+
const tick = 1000 / Math.max(arrivalRate, rampTo); // smallest tick
|
|
101
|
+
const difference = rampTo - arrivalRate;
|
|
102
|
+
const periods = duration * 1000 / tick;
|
|
103
|
+
|
|
104
|
+
function arrivalProbability(currentStep) {
|
|
105
|
+
// linear function ax + b
|
|
106
|
+
// normalized to 0 <= f(t) <= 1
|
|
107
|
+
// Anything under function value should be an arrival
|
|
108
|
+
|
|
109
|
+
let t = currentStep * tick / 1000;
|
|
110
|
+
return ((difference / duration) * t + arrivalRate) / Math.max(rampTo, arrivalRate);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let probabilities = Array.from({length: periods}, () => Math.random());
|
|
114
|
+
|
|
115
|
+
debug(
|
|
116
|
+
`rampTo: tick = ${tick}; difference = ${difference}; rampTo = ${rampTo}; arrivalRate = ${arrivalRate}; periods = ${periods}`
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return function rampTask(callback) {
|
|
120
|
+
ee.emit('phaseStarted', spec);
|
|
121
|
+
let currentStep = 1;
|
|
122
|
+
const timer = driftless.setDriftlessInterval(function maybeArrival() {
|
|
123
|
+
if (currentStep <= periods) {
|
|
124
|
+
let arrivalBreakpoint = arrivalProbability(currentStep);
|
|
125
|
+
let roll = probabilities[currentStep-1];
|
|
126
|
+
debug(`roll:${roll} <= breakpoint:${arrivalBreakpoint}`);
|
|
127
|
+
if (roll <= arrivalBreakpoint) {
|
|
128
|
+
ee.emit('arrival', spec);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
currentStep++;
|
|
132
|
+
} else {
|
|
133
|
+
driftless.clearDriftless(timer);
|
|
134
|
+
ee.emit('phaseCompleted', spec);
|
|
135
|
+
|
|
136
|
+
return callback(null);
|
|
137
|
+
}
|
|
138
|
+
}, tick);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function createArrivalCount(spec, ee) {
|
|
143
|
+
const task = function (callback) {
|
|
144
|
+
ee.emit('phaseStarted', spec);
|
|
145
|
+
const duration = spec.duration * 1000;
|
|
146
|
+
|
|
147
|
+
if (spec.arrivalCount > 0) {
|
|
148
|
+
const interval = duration / spec.arrivalCount;
|
|
149
|
+
const p = arrivals.uniform.process(interval, duration);
|
|
150
|
+
p.on('arrival', function () {
|
|
151
|
+
ee.emit('arrival', spec);
|
|
152
|
+
});
|
|
153
|
+
p.on('finished', function () {
|
|
154
|
+
ee.emit('phaseCompleted', spec);
|
|
155
|
+
return callback(null);
|
|
156
|
+
});
|
|
157
|
+
p.start();
|
|
158
|
+
} else {
|
|
159
|
+
return callback(null);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return task;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function createArrivalRate(spec, ee) {
|
|
167
|
+
const task = function (callback) {
|
|
168
|
+
ee.emit('phaseStarted', spec);
|
|
169
|
+
const ar = 1000 / spec.arrivalRate;
|
|
170
|
+
const duration = spec.duration * 1000;
|
|
171
|
+
debug('creating a %s process for arrivalRate', spec.mode);
|
|
172
|
+
const p = arrivals[spec.mode].process(ar, duration);
|
|
173
|
+
p.on('arrival', function () {
|
|
174
|
+
ee.emit('arrival', spec);
|
|
175
|
+
});
|
|
176
|
+
p.on('finished', function () {
|
|
177
|
+
ee.emit('phaseCompleted', spec);
|
|
178
|
+
return callback(null);
|
|
179
|
+
});
|
|
180
|
+
p.start();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return task;
|
|
184
|
+
}
|
package/lib/readers.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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 _ = require('lodash');
|
|
8
|
+
|
|
9
|
+
module.exports = createReader;
|
|
10
|
+
|
|
11
|
+
function createReader(order, spec) {
|
|
12
|
+
if (order === 'sequence') {
|
|
13
|
+
return createSequencedReader();
|
|
14
|
+
} else if (typeof order === 'undefined' && (typeof spec?.name !== 'undefined') && spec?.loadAll === true) {
|
|
15
|
+
return createEverythingReader(spec);
|
|
16
|
+
} else { // random
|
|
17
|
+
return createRandomReader();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createSequencedReader() {
|
|
22
|
+
let i = 0;
|
|
23
|
+
return function (data) {
|
|
24
|
+
let result = data[i];
|
|
25
|
+
if (i < data.length - 1) {
|
|
26
|
+
i++;
|
|
27
|
+
} else {
|
|
28
|
+
i = 0;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createEverythingReader(spec) {
|
|
35
|
+
let parsedData;
|
|
36
|
+
|
|
37
|
+
return function (data) {
|
|
38
|
+
if (!parsedData) {
|
|
39
|
+
parsedData = [];
|
|
40
|
+
|
|
41
|
+
// Parse the row into an object based on the fields spec
|
|
42
|
+
if (spec.fields?.length > 0) {
|
|
43
|
+
for (const row of data) {
|
|
44
|
+
let o = {};
|
|
45
|
+
for(let i = 0; i < spec.fields.length; i++) {
|
|
46
|
+
const fieldName = spec.fields[i];
|
|
47
|
+
o[fieldName] = row[i];
|
|
48
|
+
}
|
|
49
|
+
parsedData.push(o);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Otherwise just return the array of rows
|
|
53
|
+
parsedData = data;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parsedData;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createRandomReader() {
|
|
62
|
+
return function (data) {
|
|
63
|
+
return data[Math.max(0, _.random(0, data.length - 1))];
|
|
64
|
+
};
|
|
65
|
+
}
|