@depup/artillery 2.0.30-depup.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.
Files changed (90) hide show
  1. package/README.md +63 -0
  2. package/bin/run +29 -0
  3. package/bin/run.cmd +3 -0
  4. package/changes.json +138 -0
  5. package/console-reporter.js +1 -0
  6. package/lib/artillery-global.js +33 -0
  7. package/lib/cli/banner.js +8 -0
  8. package/lib/cli/common-flags.js +80 -0
  9. package/lib/cli/hooks/version.js +20 -0
  10. package/lib/cmds/dino.js +109 -0
  11. package/lib/cmds/quick.js +122 -0
  12. package/lib/cmds/report.js +34 -0
  13. package/lib/cmds/run-aci.js +91 -0
  14. package/lib/cmds/run-fargate.js +192 -0
  15. package/lib/cmds/run-lambda.js +96 -0
  16. package/lib/cmds/run.js +671 -0
  17. package/lib/console-capture.js +92 -0
  18. package/lib/console-reporter.js +438 -0
  19. package/lib/create-bom/built-in-plugins.js +12 -0
  20. package/lib/create-bom/create-bom.js +301 -0
  21. package/lib/dispatcher.js +9 -0
  22. package/lib/dist.js +222 -0
  23. package/lib/index.js +5 -0
  24. package/lib/launch-platform.js +439 -0
  25. package/lib/load-plugins.js +113 -0
  26. package/lib/platform/aws/aws-cloudwatch.js +106 -0
  27. package/lib/platform/aws/aws-create-sqs-queue.js +58 -0
  28. package/lib/platform/aws/aws-ensure-s3-bucket-exists.js +78 -0
  29. package/lib/platform/aws/aws-get-account-id.js +26 -0
  30. package/lib/platform/aws/aws-get-bucket-region.js +18 -0
  31. package/lib/platform/aws/aws-get-credentials.js +28 -0
  32. package/lib/platform/aws/aws-get-default-region.js +26 -0
  33. package/lib/platform/aws/aws-whoami.js +15 -0
  34. package/lib/platform/aws/constants.js +7 -0
  35. package/lib/platform/aws/iam-cf-templates/aws-iam-fargate-cf-template.yml +219 -0
  36. package/lib/platform/aws/iam-cf-templates/aws-iam-lambda-cf-template.yml +125 -0
  37. package/lib/platform/aws/iam-cf-templates/gh-oidc-fargate.yml +241 -0
  38. package/lib/platform/aws/iam-cf-templates/gh-oidc-lambda.yml +153 -0
  39. package/lib/platform/aws-ecs/ecs.js +247 -0
  40. package/lib/platform/aws-ecs/legacy/aws-util.js +134 -0
  41. package/lib/platform/aws-ecs/legacy/bom.js +528 -0
  42. package/lib/platform/aws-ecs/legacy/constants.js +27 -0
  43. package/lib/platform/aws-ecs/legacy/create-s3-client.js +24 -0
  44. package/lib/platform/aws-ecs/legacy/create-test.js +247 -0
  45. package/lib/platform/aws-ecs/legacy/errors.js +34 -0
  46. package/lib/platform/aws-ecs/legacy/find-public-subnets.js +149 -0
  47. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-inspect-script/index.js +27 -0
  48. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js +80 -0
  49. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js +202 -0
  50. package/lib/platform/aws-ecs/legacy/plugins.js +16 -0
  51. package/lib/platform/aws-ecs/legacy/run-cluster.js +1994 -0
  52. package/lib/platform/aws-ecs/legacy/sqs-reporter.js +401 -0
  53. package/lib/platform/aws-ecs/legacy/tags.js +22 -0
  54. package/lib/platform/aws-ecs/legacy/test-run-status.js +9 -0
  55. package/lib/platform/aws-ecs/legacy/time.js +67 -0
  56. package/lib/platform/aws-ecs/legacy/util.js +97 -0
  57. package/lib/platform/aws-ecs/worker/Dockerfile +64 -0
  58. package/lib/platform/aws-ecs/worker/helpers.sh +80 -0
  59. package/lib/platform/aws-ecs/worker/loadgen-worker +656 -0
  60. package/lib/platform/aws-lambda/dependencies.js +130 -0
  61. package/lib/platform/aws-lambda/index.js +734 -0
  62. package/lib/platform/aws-lambda/lambda-handler/a9-handler-dependencies.js +73 -0
  63. package/lib/platform/aws-lambda/lambda-handler/a9-handler-helpers.js +43 -0
  64. package/lib/platform/aws-lambda/lambda-handler/a9-handler-index.js +235 -0
  65. package/lib/platform/aws-lambda/lambda-handler/package.json +15 -0
  66. package/lib/platform/aws-lambda/prices.js +29 -0
  67. package/lib/platform/az/aci.js +694 -0
  68. package/lib/platform/az/aqs-queue-consumer.js +88 -0
  69. package/lib/platform/az/regions.js +52 -0
  70. package/lib/platform/cloud/api.js +72 -0
  71. package/lib/platform/cloud/cloud.js +448 -0
  72. package/lib/platform/cloud/http-client.js +19 -0
  73. package/lib/platform/local/artillery-worker-local.js +154 -0
  74. package/lib/platform/local/index.js +174 -0
  75. package/lib/platform/local/worker.js +261 -0
  76. package/lib/platform/worker-states.js +13 -0
  77. package/lib/queue-consumer/index.js +56 -0
  78. package/lib/stash.js +41 -0
  79. package/lib/telemetry.js +78 -0
  80. package/lib/util/await-on-ee.js +24 -0
  81. package/lib/util/generate-id.js +9 -0
  82. package/lib/util/parse-tag-string.js +21 -0
  83. package/lib/util/prepare-test-execution-plan.js +216 -0
  84. package/lib/util/sleep.js +7 -0
  85. package/lib/util/validate-script.js +132 -0
  86. package/lib/util.js +294 -0
  87. package/lib/utils-config.js +31 -0
  88. package/package.json +323 -0
  89. package/types.d.ts +317 -0
  90. package/util.js +1 -0
@@ -0,0 +1,154 @@
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
+ const EventEmitter = require('eventemitter3');
6
+ const { Worker } = require('node:worker_threads');
7
+ const path = require('node:path');
8
+
9
+ const STATES = require('../worker-states');
10
+
11
+ const awaitOnEE = require('../../util/await-on-ee');
12
+
13
+ const returnWorkerEnv = (needsSourcemap) => {
14
+ const env = { ...process.env };
15
+
16
+ if (needsSourcemap) {
17
+ env.NODE_OPTIONS = process.env.NODE_OPTIONS
18
+ ? `${process.env.NODE_OPTIONS} --enable-source-maps`
19
+ : '--enable-source-maps';
20
+ }
21
+
22
+ return env;
23
+ };
24
+
25
+ class ArtilleryWorker {
26
+ constructor(opts) {
27
+ this.opts = opts;
28
+ this.events = new EventEmitter(); // events for consumers of this object
29
+ this.workerEvents = new EventEmitter(); // turn events delivered via 'message' events into their own messages
30
+ }
31
+
32
+ async init(_opts) {
33
+ this.state = STATES.initializing;
34
+
35
+ const workerEnv = returnWorkerEnv(global.artillery.hasTypescriptProcessor);
36
+
37
+ this.worker = new Worker(path.join(__dirname, 'worker.js'), {
38
+ env: workerEnv
39
+ });
40
+ this.workerId = this.worker.threadId;
41
+ this.worker.on('error', this.onError.bind(this));
42
+ // TODO:
43
+ this.worker.on('exit', (exitCode) => {
44
+ this.events.emit('exit', exitCode);
45
+ });
46
+
47
+ this.worker.on('messageerror', (_err) => {});
48
+
49
+ // TODO: Expose performance metrics via getHeapSnapshot() and performance object.
50
+
51
+ await awaitOnEE(this.worker, 'online', 10);
52
+
53
+ // Relay messages onto the real event emitter:
54
+ this.worker.on('message', (message) => {
55
+ switch (message.event) {
56
+ case 'log':
57
+ this.events.emit('log', message);
58
+ this.workerEvents.emit('log', message);
59
+ break;
60
+ case 'workerError':
61
+ this.events.emit('workerError', message);
62
+ this.workerEvents.emit('workerError', message);
63
+ break;
64
+ case 'phaseStarted':
65
+ this.events.emit('phaseStarted', message);
66
+ this.workerEvents.emit('phaseStarted', message);
67
+ break;
68
+ case 'phaseCompleted':
69
+ this.events.emit('phaseCompleted', message);
70
+ this.workerEvents.emit('phaseCompleted', message);
71
+ break;
72
+ case 'stats':
73
+ this.events.emit('stats', message);
74
+ this.workerEvents.emit('stats', message);
75
+ break;
76
+ case 'done':
77
+ this.events.emit('done', message);
78
+ this.workerEvents.emit('done', message);
79
+ break;
80
+ case 'running':
81
+ this.events.emit('running', message);
82
+ this.workerEvents.emit('running', message);
83
+ break;
84
+ case 'readyWaiting':
85
+ this.events.emit('readyWaiting', message);
86
+ this.workerEvents.emit('readyWaiting', message);
87
+ break;
88
+ case 'setSuggestedExitCode':
89
+ this.events.emit('setSuggestedExitCode', message);
90
+ break;
91
+ default:
92
+ global.artillery.log(
93
+ `Unknown message from worker ${message}`,
94
+ 'error'
95
+ );
96
+ }
97
+ });
98
+
99
+ this.state = STATES.online;
100
+ }
101
+
102
+ async prepare(opts) {
103
+ this.state = STATES.preparing;
104
+
105
+ const { script, payload, options } = opts;
106
+ let scriptForWorker = script;
107
+
108
+ if (script.__transpiledTypeScriptPath && script.__originalScriptPath) {
109
+ scriptForWorker = {
110
+ __transpiledTypeScriptPath: script.__transpiledTypeScriptPath,
111
+ __originalScriptPath: script.__originalScriptPath,
112
+ __phases: script.config?.phases
113
+ };
114
+ }
115
+
116
+ this.worker.postMessage({
117
+ command: 'prepare',
118
+ opts: {
119
+ script: scriptForWorker,
120
+ payload,
121
+ options,
122
+ testRunId: global.artillery.testRunId
123
+ }
124
+ });
125
+
126
+ await awaitOnEE(this.workerEvents, 'readyWaiting', 50);
127
+ this.state = STATES.readyWaiting;
128
+ }
129
+
130
+ async run(opts) {
131
+ this.worker.postMessage({
132
+ command: 'run',
133
+ opts: JSON.parse(opts)
134
+ });
135
+
136
+ await awaitOnEE(this.workerEvents, 'running', 50);
137
+ this.state = STATES.running;
138
+ }
139
+
140
+ async stop() {
141
+ this.worker.postMessage({ command: 'stop' });
142
+ }
143
+
144
+ onError(err) {
145
+ // TODO: set state, clean up
146
+ this.events.emit('error', err);
147
+ console.log('worker error, id:', this.workerId, err);
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ ArtilleryWorker,
153
+ STATES
154
+ };
@@ -0,0 +1,174 @@
1
+ const { ArtilleryWorker } = require('./artillery-worker-local');
2
+ const core = require('../../dispatcher');
3
+ const { handleScriptHook, prepareScript, loadProcessor } =
4
+ core.runner.runnerFuncs;
5
+ const debug = require('debug')('platform:local');
6
+ const EventEmitter = require('node:events');
7
+ const _ = require('lodash');
8
+ const divideWork = require('../../dist');
9
+ const STATES = require('../worker-states');
10
+ const os = require('node:os');
11
+ class PlatformLocal {
12
+ constructor(script, payload, opts, platformOpts) {
13
+ // We need these to run before/after hooks:
14
+ this.script = script;
15
+ this.payload = payload;
16
+ this.opts = opts;
17
+ this.events = new EventEmitter(); // send worker events such as workerError, etc
18
+ this.platformOpts = platformOpts;
19
+ this.workers = {};
20
+ this.workerScripts = {};
21
+ this.count = Infinity;
22
+ }
23
+
24
+ getDesiredWorkerCount() {
25
+ return this.count;
26
+ }
27
+
28
+ async startJob() {
29
+ await this.init();
30
+
31
+ if (this.platformOpts.mode === 'distribute') {
32
+ // Disable worker threads for Playwright-based load tests
33
+ const count = this.script.config.engines?.playwright
34
+ ? 1
35
+ : Math.max(1, os.cpus().length - 1);
36
+ this.workerScripts = divideWork(this.script, count);
37
+ this.count = this.workerScripts.length;
38
+ } else {
39
+ // --count may only be used when mode is "multiply"
40
+ this.count = this.platformOpts.count;
41
+ this.workerScripts = new Array(this.count).fill().map((_) => this.script);
42
+ }
43
+
44
+ for (const script of this.workerScripts) {
45
+ const w1 = await this.createWorker();
46
+
47
+ this.workers[w1.workerId] = {
48
+ id: w1.workerId,
49
+ script,
50
+ state: STATES.initializing,
51
+ proc: w1
52
+ };
53
+ debug(`worker init ok: ${w1.workerId}`);
54
+ }
55
+
56
+ for (const [workerId, w] of Object.entries(this.workers)) {
57
+ this.opts.cliArgs = this.platformOpts.cliArgs;
58
+ await this.prepareWorker(workerId, {
59
+ script: w.script,
60
+ payload: this.payload,
61
+ options: this.opts
62
+ });
63
+ this.workers[workerId].state = STATES.preparing;
64
+ }
65
+ debug('workers prepared');
66
+
67
+ // the initial context is stringified and copied to the workers
68
+ const contextVarsString = JSON.stringify(this.contextVars);
69
+
70
+ for (const [workerId, _w] of Object.entries(this.workers)) {
71
+ await this.runWorker(workerId, contextVarsString);
72
+ this.workers[workerId].state = STATES.initializing;
73
+ }
74
+ }
75
+
76
+ async init() {
77
+ // 'before' hook is executed in the main thread,
78
+ // its context is then passed to the workers
79
+ const contextVars = await this.runHook('before');
80
+ this.contextVars = contextVars; // TODO: Rename to something more descriptive
81
+ }
82
+
83
+ async createWorker() {
84
+ const worker = new ArtilleryWorker();
85
+
86
+ await worker.init();
87
+
88
+ const workerId = worker.workerId;
89
+ worker.events.on('workerError', (message) => {
90
+ this.events.emit('workerError', workerId, message);
91
+ });
92
+ worker.events.on('log', (message) => {
93
+ this.events.emit('log', workerId, message);
94
+ });
95
+ worker.events.on('phaseStarted', (message) => {
96
+ this.events.emit('phaseStarted', workerId, message);
97
+ });
98
+ worker.events.on('phaseCompleted', (message) => {
99
+ this.events.emit('phaseCompleted', workerId, message);
100
+ });
101
+ worker.events.on('stats', (message) => {
102
+ this.events.emit('stats', workerId, message);
103
+ });
104
+ worker.events.on('done', (message) => {
105
+ this.events.emit('done', workerId, message);
106
+ });
107
+ worker.events.on('readyWaiting', (message) => {
108
+ this.events.emit('readyWaiting', workerId, message);
109
+ });
110
+ worker.events.on('setSuggestedExitCode', (message) => {
111
+ this.events.emit('setSuggestedExitCode', workerId, message);
112
+ });
113
+ worker.events.on('exit', (message) => {
114
+ this.events.emit('exit', workerId, message);
115
+ });
116
+
117
+ worker.events.on('error', (_err) => {
118
+ // TODO: Only exit if ALL workers fail, otherwise log and carry on
119
+ process.nextTick(() => process.exit(11));
120
+ });
121
+
122
+ return worker;
123
+ }
124
+
125
+ async prepareWorker(workerId, opts) {
126
+ return this.workers[workerId].proc.prepare(opts);
127
+ }
128
+
129
+ async runWorker(workerId, contextVarsString) {
130
+ // TODO: this will become opts
131
+ debug('runWorker', workerId);
132
+ return this.workers[workerId].proc.run(contextVarsString);
133
+ }
134
+
135
+ async stopWorker(workerId) {
136
+ return this.workers[workerId].proc.stop();
137
+ }
138
+
139
+ async shutdown() {
140
+ // 'after' hook is executed in the main thread, after all workers
141
+ // are done
142
+ await this.runHook('after', this.contextVars);
143
+
144
+ for (const [workerId, _w] of Object.entries(this.workers)) {
145
+ await this.stopWorker(workerId);
146
+ }
147
+ }
148
+
149
+ // ********
150
+
151
+ async runHook(hook, initialContextVars) {
152
+ if (!this.script[hook]) {
153
+ return {};
154
+ }
155
+
156
+ const runnableScript = await loadProcessor(
157
+ prepareScript(this.script, _.cloneDeep(this.payload)),
158
+ this.opts
159
+ );
160
+
161
+ const contextVars = await handleScriptHook(
162
+ hook,
163
+ runnableScript,
164
+ this.events,
165
+ initialContextVars
166
+ );
167
+
168
+ debug(`hook ${hook} context vars`, contextVars);
169
+
170
+ return contextVars;
171
+ }
172
+ }
173
+
174
+ module.exports = PlatformLocal;
@@ -0,0 +1,261 @@
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
+ //
6
+ // Artillery Core worker process
7
+ //
8
+
9
+
10
+
11
+ const {
12
+ parentPort,
13
+ threadId
14
+ } = require('node:worker_threads');
15
+
16
+ const { getStash } = require('../../../lib/stash');
17
+
18
+ const { createGlobalObject } = require('../../artillery-global');
19
+
20
+ const core = require('@artilleryio/int-core');
21
+ const createRunner = core.runner.runner;
22
+ const debug = require('debug')('artillery:worker');
23
+
24
+ const _path = require('node:path');
25
+
26
+ const { SSMS } = require('@artilleryio/int-core').ssms;
27
+ const { loadPlugins, loadPluginsConfig } = require('../../load-plugins');
28
+
29
+ const EventEmitter = require('eventemitter3');
30
+ const p = require('node:util').promisify;
31
+ const { loadProcessor } = core.runner.runnerFuncs;
32
+
33
+ const prepareTestExecutionPlan = require('../../util/prepare-test-execution-plan');
34
+
35
+ process.env.LOCAL_WORKER_ID = threadId;
36
+
37
+ parentPort.on('message', onMessage);
38
+
39
+ let shuttingDown = false;
40
+
41
+ let runnerInstance = null;
42
+
43
+ global.artillery._workerThreadSend = send;
44
+
45
+ //
46
+ // Supported messages: run, stop
47
+ //
48
+
49
+ async function onMessage(message) {
50
+ if (message.command === 'prepare') {
51
+ await prepare(message.opts);
52
+ return;
53
+ }
54
+
55
+ if (message.command === 'run') {
56
+ run(message.opts);
57
+ return;
58
+ }
59
+
60
+ if (message.command === 'stop') {
61
+ await cleanup();
62
+
63
+ // Unload plugins
64
+ // TODO: v3 plugins
65
+ for (const o of global.artillery.plugins) {
66
+ if (o.plugin.cleanup) {
67
+ try {
68
+ await p(o.plugin.cleanup.bind(o.plugin))();
69
+ debug('plugin unloaded:', o.name);
70
+ } catch (cleanupErr) {
71
+ send({
72
+ event: 'workerError',
73
+ error: cleanupErr,
74
+ level: 'error',
75
+ aggregatable: true
76
+ });
77
+ }
78
+ }
79
+ }
80
+
81
+ process.exit(0);
82
+ }
83
+ }
84
+
85
+ async function cleanup() {
86
+ return new Promise((resolve, _reject) => {
87
+ if (shuttingDown) {
88
+ resolve();
89
+ }
90
+ shuttingDown = true;
91
+
92
+ if (runnerInstance && typeof runnerInstance.stop === 'function') {
93
+ runnerInstance.stop().then(() => {
94
+ resolve();
95
+ });
96
+ } else {
97
+ resolve();
98
+ }
99
+ });
100
+ }
101
+
102
+ async function createGlobalStashClient(cliArgs) {
103
+ try {
104
+ global.artillery.stash = await getStash({
105
+ apiKey: cliArgs?.key || process.env.ARTILLERY_CLOUD_API_KEY
106
+ });
107
+ } catch (error) {
108
+ if (error.name !== 'CloudAPIKeyMissing') {
109
+ console.error(error);
110
+ }
111
+ global.artillery.stash = null;
112
+ }
113
+ }
114
+
115
+ async function prepare(opts) {
116
+ await createGlobalObject();
117
+ await createGlobalStashClient(opts.options.cliArgs);
118
+
119
+ global.artillery.globalEvents.on('log', (...args) => {
120
+ send({ event: 'log', args });
121
+ });
122
+
123
+ let _script;
124
+ if (
125
+ opts.script.__transpiledTypeScriptPath &&
126
+ opts.script.__originalScriptPath
127
+ ) {
128
+ // Load and process pre-compiled TypeScript file
129
+ _script = await prepareTestExecutionPlan(
130
+ [opts.script.__originalScriptPath],
131
+ opts.options.cliArgs,
132
+ []
133
+ );
134
+ } else {
135
+ _script = opts.script;
136
+ }
137
+
138
+ const { payload, options } = opts;
139
+ const script = await loadProcessor(_script, options);
140
+
141
+ if (opts.script.__phases) {
142
+ script.config.phases = opts.script.__phases;
143
+ }
144
+
145
+ global.artillery.testRunId = opts.testRunId;
146
+
147
+ //
148
+ // load plugins
149
+ //
150
+ const plugins = await loadPlugins(script.config.plugins, script, options);
151
+
152
+ // NOTE: We don't subscribe plugins to stats/done events from
153
+ // individual runner instances here - those are handled in
154
+ // launch-platform instead. (If we subscribe plugins to events here,
155
+ // they will receive individual stats/done events from workers,
156
+ // instead of objects that have been properly aggregated.)
157
+ const stubEE = new EventEmitter();
158
+ for (const [name, result] of Object.entries(plugins)) {
159
+ if (result.isLoaded) {
160
+ global.artillery.plugins[name] = result.plugin;
161
+ if (result.version === 3) {
162
+ // TODO: v3 plugins
163
+ } else {
164
+ // const msg = `WARNING: Legacy plugin detected: ${name}
165
+ // See https://artillery.io/docs/resources/core/v2.html for more details.`;
166
+ // send({
167
+ // event: 'workerError',
168
+ // error: new Error(msg),
169
+ // level: 'warn',
170
+ // aggregatable: true
171
+ // });
172
+
173
+ script.config = {
174
+ ...script.config,
175
+ // Load additional plugins configuration from the environment
176
+ plugins: loadPluginsConfig(script.config.plugins)
177
+ };
178
+
179
+ if (result.version === 1) {
180
+ result.plugin = new result.PluginExport(script.config, stubEE);
181
+ global.artillery.plugins.push(result);
182
+ } else if (result.version === 2) {
183
+ result.plugin = new result.PluginExport.Plugin(
184
+ script,
185
+ stubEE,
186
+ options
187
+ );
188
+ global.artillery.plugins.push(result);
189
+ } else {
190
+ // TODO:
191
+ }
192
+ }
193
+ } else {
194
+ const msg = `WARNING: Could not load plugin: ${name}`;
195
+ send({
196
+ event: 'workerError',
197
+ error: new Error(msg),
198
+ level: 'warn',
199
+ aggregatable: true
200
+ });
201
+ }
202
+ }
203
+
204
+ // TODO: use await
205
+ createRunner(script, payload, options)
206
+ .then((runner) => {
207
+ runnerInstance = runner;
208
+
209
+ runner.on('phaseStarted', onPhaseStarted);
210
+ runner.on('phaseCompleted', onPhaseCompleted);
211
+ runner.on('stats', onStats);
212
+ runner.on('done', onDone);
213
+
214
+ // TODO: Enum for all event types
215
+ send({ event: 'readyWaiting' });
216
+ })
217
+ .catch((err) => {
218
+ // TODO: Clean up and exit (error state)
219
+ // TODO: Handle workerError in launcher when readyWaiting
220
+ // is not received and worker exits.
221
+ send({
222
+ event: 'workerError',
223
+ error: err,
224
+ level: 'error',
225
+ aggregatable: true
226
+ });
227
+ });
228
+
229
+ function onPhaseStarted(phase) {
230
+ send({ event: 'phaseStarted', phase: phase });
231
+ }
232
+
233
+ function onPhaseCompleted(phase) {
234
+ send({ event: 'phaseCompleted', phase: phase });
235
+ }
236
+
237
+ function onStats(stats) {
238
+ send({ event: 'stats', stats: SSMS.serializeMetrics(stats) });
239
+ }
240
+
241
+ async function onDone(report) {
242
+ await runnerInstance.stop();
243
+ send({ event: 'done', report: SSMS.serializeMetrics(report) });
244
+ }
245
+ }
246
+
247
+ async function run(opts) {
248
+ if (runnerInstance) {
249
+ runnerInstance.run(opts);
250
+ send({ event: 'running' });
251
+ } else {
252
+ // TODO: Emit error / set state
253
+ }
254
+ }
255
+
256
+ // TODO: id -> workerId, ts -> _ts
257
+ function send(data) {
258
+ const payload = Object.assign({ id: threadId, ts: Date.now() }, data);
259
+ debug(payload);
260
+ parentPort.postMessage(payload);
261
+ }
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ initializing: 1,
3
+ online: 2,
4
+ preparing: 3,
5
+ readyWaiting: 4,
6
+ running: 5,
7
+ unknown: 6,
8
+ stoppedError: 7,
9
+ completed: 8,
10
+ stoppedEarly: 9,
11
+ stoppedFailed: 10,
12
+ timedout: 11
13
+ };
@@ -0,0 +1,56 @@
1
+ const { EventEmitter } = require('eventemitter3');
2
+ const debug = require('debug')('queue-consumer');
3
+ const { Consumer } = require('sqs-consumer');
4
+
5
+ class QueueConsumer extends EventEmitter {
6
+ create(opts = { poolSize: 30 }, queueConsumerOpts) {
7
+ this.events = new EventEmitter();
8
+
9
+ this.consumers = [];
10
+
11
+ for (let i = 0; i < opts.poolSize; i++) {
12
+ const sqsConsumer = Consumer.create(queueConsumerOpts);
13
+
14
+ sqsConsumer.on('error', (err) => {
15
+ // TODO: Ignore "SQSError: SQS delete message failed:" errors
16
+
17
+ if (err.message?.match(/ReceiptHandle.+expired/i)) {
18
+ debug(err.name, err.message);
19
+ } else {
20
+ sqsConsumer.stop();
21
+ this.emit('error', err);
22
+ }
23
+ });
24
+
25
+ let empty = 0;
26
+ sqsConsumer.on('empty', () => {
27
+ empty++;
28
+ if (empty > 10) {
29
+ this.emit('messageReceiveTimeout'); // TODO:
30
+ }
31
+ });
32
+
33
+ this.consumers.push(sqsConsumer);
34
+ }
35
+
36
+ return this;
37
+ }
38
+
39
+ constructor(_opts) {
40
+ super();
41
+ }
42
+
43
+ start() {
44
+ for (const consumer of this.consumers) {
45
+ consumer.start();
46
+ }
47
+ }
48
+
49
+ stop() {
50
+ for (const consumer of this.consumers) {
51
+ consumer.stop();
52
+ }
53
+ }
54
+ }
55
+
56
+ module.exports = { QueueConsumer };
package/lib/stash.js ADDED
@@ -0,0 +1,41 @@
1
+ const { Redis } = require('@upstash/redis');
2
+ const { createClient } = require('./platform/cloud/api');
3
+
4
+ async function init(details) {
5
+ if (details) {
6
+ return new Redis({ url: details.url, token: details.token });
7
+ } else {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Get an Artillery Stash client instance
14
+ *
15
+ *
16
+ * @param {Object} options - Configuration options
17
+ * @param {string} options.apiKey - Artillery Cloud API key (optional, can use ARTILLERY_CLOUD_API_KEY env var)
18
+ * @returns {Promise<Redis|null>} - Redis client instance or null if not available
19
+ */
20
+ async function getStash(options = {}) {
21
+ const cloud = createClient({
22
+ apiKey: options.apiKey || process.env.ARTILLERY_CLOUD_API_KEY
23
+ });
24
+
25
+ const whoami = await cloud.whoami();
26
+ if (!whoami.activeOrg) {
27
+ return null;
28
+ }
29
+
30
+ const stashDetails = await cloud.getStashDetails({
31
+ orgId: whoami.activeOrg
32
+ });
33
+
34
+ if (!stashDetails) {
35
+ return null;
36
+ }
37
+
38
+ return init(stashDetails);
39
+ }
40
+
41
+ module.exports = { initStash: init, getStash };