@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.
- package/README.md +63 -0
- package/bin/run +29 -0
- package/bin/run.cmd +3 -0
- package/changes.json +138 -0
- package/console-reporter.js +1 -0
- package/lib/artillery-global.js +33 -0
- package/lib/cli/banner.js +8 -0
- package/lib/cli/common-flags.js +80 -0
- package/lib/cli/hooks/version.js +20 -0
- package/lib/cmds/dino.js +109 -0
- package/lib/cmds/quick.js +122 -0
- package/lib/cmds/report.js +34 -0
- package/lib/cmds/run-aci.js +91 -0
- package/lib/cmds/run-fargate.js +192 -0
- package/lib/cmds/run-lambda.js +96 -0
- package/lib/cmds/run.js +671 -0
- package/lib/console-capture.js +92 -0
- package/lib/console-reporter.js +438 -0
- package/lib/create-bom/built-in-plugins.js +12 -0
- package/lib/create-bom/create-bom.js +301 -0
- package/lib/dispatcher.js +9 -0
- package/lib/dist.js +222 -0
- package/lib/index.js +5 -0
- package/lib/launch-platform.js +439 -0
- package/lib/load-plugins.js +113 -0
- package/lib/platform/aws/aws-cloudwatch.js +106 -0
- package/lib/platform/aws/aws-create-sqs-queue.js +58 -0
- package/lib/platform/aws/aws-ensure-s3-bucket-exists.js +78 -0
- package/lib/platform/aws/aws-get-account-id.js +26 -0
- package/lib/platform/aws/aws-get-bucket-region.js +18 -0
- package/lib/platform/aws/aws-get-credentials.js +28 -0
- package/lib/platform/aws/aws-get-default-region.js +26 -0
- package/lib/platform/aws/aws-whoami.js +15 -0
- package/lib/platform/aws/constants.js +7 -0
- package/lib/platform/aws/iam-cf-templates/aws-iam-fargate-cf-template.yml +219 -0
- package/lib/platform/aws/iam-cf-templates/aws-iam-lambda-cf-template.yml +125 -0
- package/lib/platform/aws/iam-cf-templates/gh-oidc-fargate.yml +241 -0
- package/lib/platform/aws/iam-cf-templates/gh-oidc-lambda.yml +153 -0
- package/lib/platform/aws-ecs/ecs.js +247 -0
- package/lib/platform/aws-ecs/legacy/aws-util.js +134 -0
- package/lib/platform/aws-ecs/legacy/bom.js +528 -0
- package/lib/platform/aws-ecs/legacy/constants.js +27 -0
- package/lib/platform/aws-ecs/legacy/create-s3-client.js +24 -0
- package/lib/platform/aws-ecs/legacy/create-test.js +247 -0
- package/lib/platform/aws-ecs/legacy/errors.js +34 -0
- package/lib/platform/aws-ecs/legacy/find-public-subnets.js +149 -0
- package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-inspect-script/index.js +27 -0
- package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js +80 -0
- package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js +202 -0
- package/lib/platform/aws-ecs/legacy/plugins.js +16 -0
- package/lib/platform/aws-ecs/legacy/run-cluster.js +1994 -0
- package/lib/platform/aws-ecs/legacy/sqs-reporter.js +401 -0
- package/lib/platform/aws-ecs/legacy/tags.js +22 -0
- package/lib/platform/aws-ecs/legacy/test-run-status.js +9 -0
- package/lib/platform/aws-ecs/legacy/time.js +67 -0
- package/lib/platform/aws-ecs/legacy/util.js +97 -0
- package/lib/platform/aws-ecs/worker/Dockerfile +64 -0
- package/lib/platform/aws-ecs/worker/helpers.sh +80 -0
- package/lib/platform/aws-ecs/worker/loadgen-worker +656 -0
- package/lib/platform/aws-lambda/dependencies.js +130 -0
- package/lib/platform/aws-lambda/index.js +734 -0
- package/lib/platform/aws-lambda/lambda-handler/a9-handler-dependencies.js +73 -0
- package/lib/platform/aws-lambda/lambda-handler/a9-handler-helpers.js +43 -0
- package/lib/platform/aws-lambda/lambda-handler/a9-handler-index.js +235 -0
- package/lib/platform/aws-lambda/lambda-handler/package.json +15 -0
- package/lib/platform/aws-lambda/prices.js +29 -0
- package/lib/platform/az/aci.js +694 -0
- package/lib/platform/az/aqs-queue-consumer.js +88 -0
- package/lib/platform/az/regions.js +52 -0
- package/lib/platform/cloud/api.js +72 -0
- package/lib/platform/cloud/cloud.js +448 -0
- package/lib/platform/cloud/http-client.js +19 -0
- package/lib/platform/local/artillery-worker-local.js +154 -0
- package/lib/platform/local/index.js +174 -0
- package/lib/platform/local/worker.js +261 -0
- package/lib/platform/worker-states.js +13 -0
- package/lib/queue-consumer/index.js +56 -0
- package/lib/stash.js +41 -0
- package/lib/telemetry.js +78 -0
- package/lib/util/await-on-ee.js +24 -0
- package/lib/util/generate-id.js +9 -0
- package/lib/util/parse-tag-string.js +21 -0
- package/lib/util/prepare-test-execution-plan.js +216 -0
- package/lib/util/sleep.js +7 -0
- package/lib/util/validate-script.js +132 -0
- package/lib/util.js +294 -0
- package/lib/utils-config.js +31 -0
- package/package.json +323 -0
- package/types.d.ts +317 -0
- 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,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 };
|