@artilleryio/int-core 2.27.0 → 2.28.0-670d2fa

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.
@@ -0,0 +1,213 @@
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
+ import { randomUUID } from 'node:crypto';
5
+ import arrivals from 'arrivals';
6
+ import async from 'async';
7
+ import createDebug from 'debug';
8
+ import driftless from 'driftless';
9
+ import { EventEmitter } from 'eventemitter3';
10
+ import _ from 'lodash';
11
+ import ms from 'ms';
12
+ const debug = createDebug('phases');
13
+ const isUndefined = _.isUndefined;
14
+ export default phaser;
15
+ async function sleep(ms) {
16
+ return new Promise((resolve) => {
17
+ setTimeout(resolve, ms);
18
+ });
19
+ }
20
+ function phaser(phaseSpecs) {
21
+ const ee = new EventEmitter();
22
+ const tasks = _.map(phaseSpecs, (spec, i) => {
23
+ [
24
+ 'arrivalRate',
25
+ 'arrivalCount',
26
+ 'pause',
27
+ 'rampTo',
28
+ 'duration',
29
+ 'maxVusers'
30
+ ].forEach((k) => {
31
+ if (isUndefined(spec[k]) || spec[k] === 'number') {
32
+ return;
33
+ }
34
+ if (k === 'duration' || k === 'pause') {
35
+ //if it's already a number in string format, don't apply ms, as it's the default behaviour, so we don't want to do ms calculations
36
+ //otherwise, ms returns the value in milliseconds, so we need to convert to seconds
37
+ const convertedDuration = Number.isInteger(_.toNumber(spec[k]))
38
+ ? spec[k]
39
+ : ms(spec[k]) / 1000;
40
+ //throw error if invalid time format to prevent test from running infinitely
41
+ if (!convertedDuration) {
42
+ throw new Error(`Invalid ${k} for phase: ${spec[k]}`);
43
+ }
44
+ spec[k] = convertedDuration;
45
+ }
46
+ // Cast defined but non-number (eg: from ENV) values
47
+ spec[k] = _.toNumber(spec[k]);
48
+ });
49
+ if (isUndefined(spec.index)) {
50
+ spec.index = i;
51
+ }
52
+ if (!isUndefined(spec.arrivalRate) && isUndefined(spec.rampTo)) {
53
+ spec.mode = spec.mode || 'uniform';
54
+ }
55
+ if (!isUndefined(spec.pause)) {
56
+ return createPause(spec, ee);
57
+ }
58
+ if (!isUndefined(spec.arrivalCount)) {
59
+ return createArrivalCount(spec, ee);
60
+ }
61
+ if (!isUndefined(spec.arrivalRate)) {
62
+ // If arrivalRate is zero and it's not a ramp, it's the same as a pause:
63
+ if (spec.arrivalRate === 0 && isUndefined(spec.rampTo)) {
64
+ return createPause(Object.assign(spec, { pause: spec.duration }), ee);
65
+ }
66
+ // If it's a ramp, create that:
67
+ if (!isUndefined(spec.rampTo)) {
68
+ return createRamp(spec, ee);
69
+ }
70
+ // Otherwise it's a plain arrival phase:
71
+ return createArrivalRate(spec, ee);
72
+ }
73
+ console.log('Unknown phase spec\n%j\nThis should not happen', spec);
74
+ });
75
+ ee.run = () => {
76
+ async.series(tasks, (err) => {
77
+ if (err) {
78
+ debug(err);
79
+ }
80
+ ee.emit('done');
81
+ });
82
+ };
83
+ return ee;
84
+ }
85
+ function createPause(spec, ee) {
86
+ const duration = spec.pause * 1000;
87
+ const task = (callback) => {
88
+ spec.startTime = Date.now();
89
+ spec.id = randomUUID();
90
+ ee.emit('phaseStarted', spec);
91
+ setTimeout(() => {
92
+ spec.endTime = Date.now();
93
+ ee.emit('phaseCompleted', spec);
94
+ return callback(null);
95
+ }, duration);
96
+ };
97
+ return task;
98
+ }
99
+ function createRamp(spec, ee) {
100
+ const duration = spec.duration || 1;
101
+ const arrivalRate = spec.arrivalRate;
102
+ const rampTo = spec.rampTo;
103
+ const worker = spec.worker;
104
+ const totalWorkers = spec.totalWorkers;
105
+ const difference = rampTo - arrivalRate;
106
+ const periods = duration;
107
+ debug(`worker ${worker} totalWorkers ${totalWorkers} arrivalRate ${arrivalRate} rampTo ${rampTo} difference ${difference} periods ${periods}`);
108
+ const periodArrivals = [];
109
+ const periodTick = [];
110
+ // if there is only one peridod we generate mean arrivals
111
+ if (periods === 1) {
112
+ const rawPeriodArrivals = (rampTo + arrivalRate) / 2;
113
+ periodArrivals[0] = adjustArrivalsByWorker(rawPeriodArrivals, totalWorkers, worker);
114
+ }
115
+ else {
116
+ // for each period we calculate the corresponding arrivals:
117
+ // knowing that arrivals(0) = arrivalRate and arrivals(duration -1) = rampTo
118
+ // then: arrivals(t) = difference / (duration-1) * t + arrivalRate
119
+ for (let i = 0; i < periods; i++) {
120
+ const rawPeriodArrivals = (difference / (duration - 1)) * i + arrivalRate;
121
+ // take into account added decimals and bump worker arrivals if needed
122
+ periodArrivals[i] = adjustArrivalsByWorker(rawPeriodArrivals, totalWorkers, worker);
123
+ // Needed ticks to get to periodArrivals in 1000ms
124
+ periodTick[i] = Math.min(Math.floor(1000 / periodArrivals[i]), 1000);
125
+ }
126
+ }
127
+ debug(`periodArrivals ${periodArrivals}`);
128
+ debug(`periodTick ${periodTick}`);
129
+ return async function rampTask(_callback) {
130
+ spec.startTime = Date.now();
131
+ spec.id = randomUUID();
132
+ ee.emit('phaseStarted', spec);
133
+ for (let period = 0; period < periods; period++) {
134
+ ticker(period);
135
+ await sleep(1000);
136
+ }
137
+ spec.endTime = Date.now();
138
+ ee.emit('phaseCompleted', spec);
139
+ };
140
+ function adjustArrivalsByWorker(rawPeriodArrivals, totalWorkers, worker) {
141
+ // We use the floor of the expected arrivals, then we add up all decimal digits
142
+ // and evaluate if one or more workers should bump their arrivalRate.
143
+ let arrivals = Math.floor(rawPeriodArrivals);
144
+ // Think of fractionalPart * workers as the amount of arrivals that could not be
145
+ // shared evenly across all workers.
146
+ if (Math.round((rawPeriodArrivals % 1) * totalWorkers) >= worker) {
147
+ arrivals = arrivals + 1;
148
+ }
149
+ return arrivals;
150
+ }
151
+ function ticker(currentPeriod) {
152
+ // ensure we don't go past 1s
153
+ const delay = Math.min(periodTick[currentPeriod], 1000);
154
+ let currentArrivals = 0;
155
+ const arrivalTimer = driftless.setDriftlessInterval(function arrivals() {
156
+ if (currentArrivals < periodArrivals[currentPeriod]) {
157
+ ee.emit('arrival', spec);
158
+ currentArrivals++;
159
+ }
160
+ else {
161
+ currentPeriod++;
162
+ driftless.clearDriftless(arrivalTimer);
163
+ }
164
+ }, delay);
165
+ return;
166
+ }
167
+ }
168
+ function createArrivalCount(spec, ee) {
169
+ const task = (callback) => {
170
+ spec.startTime = Date.now();
171
+ spec.id = randomUUID();
172
+ ee.emit('phaseStarted', spec);
173
+ const duration = spec.duration * 1000;
174
+ if (spec.arrivalCount > 0) {
175
+ const interval = duration / spec.arrivalCount;
176
+ const p = arrivals.uniform.process(interval, duration);
177
+ p.on('arrival', () => {
178
+ ee.emit('arrival', spec);
179
+ });
180
+ p.on('finished', () => {
181
+ spec.endTime = Date.now();
182
+ ee.emit('phaseCompleted', spec);
183
+ return callback(null);
184
+ });
185
+ p.start();
186
+ }
187
+ else {
188
+ return callback(null);
189
+ }
190
+ };
191
+ return task;
192
+ }
193
+ function createArrivalRate(spec, ee) {
194
+ const task = (callback) => {
195
+ spec.startTime = Date.now();
196
+ spec.id = randomUUID();
197
+ ee.emit('phaseStarted', spec);
198
+ const ar = 1000 / spec.arrivalRate;
199
+ const duration = spec.duration * 1000;
200
+ debug('creating a %s process for arrivalRate', spec.mode);
201
+ const p = arrivals[spec.mode].process(ar, duration);
202
+ p.on('arrival', () => {
203
+ ee.emit('arrival', spec);
204
+ });
205
+ p.on('finished', () => {
206
+ spec.endTime = Date.now();
207
+ ee.emit('phaseCompleted', spec);
208
+ return callback(null);
209
+ });
210
+ p.start();
211
+ };
212
+ return task;
213
+ }
@@ -0,0 +1,58 @@
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
+ import _ from 'lodash';
5
+ export default function createReader(order, spec) {
6
+ if (order === 'sequence') {
7
+ return createSequencedReader();
8
+ }
9
+ else if (typeof order === 'undefined' &&
10
+ typeof spec?.name !== 'undefined' &&
11
+ spec?.loadAll === true) {
12
+ return createEverythingReader(spec);
13
+ }
14
+ else {
15
+ // random
16
+ return createRandomReader();
17
+ }
18
+ }
19
+ function createSequencedReader() {
20
+ let i = 0;
21
+ return (data) => {
22
+ const result = data[i];
23
+ if (i < data.length - 1) {
24
+ i++;
25
+ }
26
+ else {
27
+ i = 0;
28
+ }
29
+ return result;
30
+ };
31
+ }
32
+ function createEverythingReader(spec) {
33
+ let parsedData;
34
+ return (data) => {
35
+ if (!parsedData) {
36
+ parsedData = [];
37
+ // Parse the row into an object based on the fields spec
38
+ if (spec.fields?.length > 0) {
39
+ for (const row of data) {
40
+ const o = {};
41
+ for (let i = 0; i < spec.fields.length; i++) {
42
+ const fieldName = spec.fields[i];
43
+ o[fieldName] = row[i];
44
+ }
45
+ parsedData.push(o);
46
+ }
47
+ }
48
+ else {
49
+ // Otherwise just return the array of rows
50
+ parsedData = data;
51
+ }
52
+ }
53
+ return parsedData;
54
+ };
55
+ }
56
+ function createRandomReader() {
57
+ return (data) => data[Math.max(0, _.random(0, data.length - 1))];
58
+ }