@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,1994 @@
1
+ const {
2
+ ECSClient,
3
+ CreateClusterCommand,
4
+ DescribeClustersCommand,
5
+ RunTaskCommand,
6
+ StopTaskCommand,
7
+ DescribeTaskDefinitionCommand,
8
+ RegisterTaskDefinitionCommand,
9
+ DeregisterTaskDefinitionCommand,
10
+ InvalidParameterException,
11
+ ThrottlingException
12
+ } = require('@aws-sdk/client-ecs');
13
+ const {
14
+ SQSClient,
15
+ CreateQueueCommand,
16
+ DeleteQueueCommand,
17
+ ListQueuesCommand,
18
+ GetQueueAttributesCommand
19
+ } = require('@aws-sdk/client-sqs');
20
+ const { GetObjectCommand, NoSuchKey } = require('@aws-sdk/client-s3');
21
+ const { IAMClient, GetRoleCommand } = require('@aws-sdk/client-iam');
22
+ // Normal debugging for messages, summaries, and errors:
23
+ const debug = require('debug')('commands:run-test');
24
+ // Verbose debugging for responses from AWS API calls, large objects etc:
25
+ const debugVerbose = require('debug')('commands:run-test:v');
26
+ const debugErr = require('debug')('commands:run-test:errors');
27
+ const _A = require('async');
28
+ const path = require('node:path');
29
+ const fs = require('node:fs');
30
+ const chalk = require('chalk');
31
+ const defaultOptions = require('rc')('artillery');
32
+ const moment = require('moment');
33
+
34
+ const EnsurePlugin = require('artillery-plugin-ensure');
35
+ const SlackPlugin = require('artillery-plugin-slack');
36
+
37
+ const {
38
+ getADOTRelevantReporterConfigs,
39
+ resolveADOTConfigSettings
40
+ } = require('artillery-plugin-publish-metrics');
41
+
42
+ const EventEmitter = require('node:events');
43
+
44
+ const _ = require('lodash');
45
+
46
+ const pkg = require('../../../../package.json');
47
+ const { parseTags } = require('./tags');
48
+ const { Timeout, sleep, timeStringToMs } = require('./time');
49
+ const { SqsReporter } = require('./sqs-reporter');
50
+
51
+ const awaitOnEE = require('../../../../lib/util/await-on-ee');
52
+
53
+ const { VPCSubnetFinder } = require('./find-public-subnets');
54
+ const awsUtil = require('./aws-util');
55
+ const { createTest } = require('./create-test');
56
+
57
+ const createS3Client = require('./create-s3-client');
58
+ const { getBucketName } = require('./util');
59
+ const getAccountId = require('../../aws/aws-get-account-id');
60
+ const { setCloudwatchRetention } = require('../../aws/aws-cloudwatch');
61
+
62
+ const dotenv = require('dotenv');
63
+
64
+ const util = require('./util');
65
+
66
+ module.exports = runCluster;
67
+
68
+ let consoleReporter = {
69
+ toggleSpinner: () => {}
70
+ };
71
+
72
+ const {
73
+ TASK_NAME,
74
+ SQS_QUEUES_NAME_PREFIX,
75
+ LOGGROUP_NAME,
76
+ LOGGROUP_RETENTION_DAYS,
77
+ IMAGE_VERSION,
78
+ WAIT_TIMEOUT,
79
+ ARTILLERY_CLUSTER_NAME,
80
+ TEST_RUNS_MAX_TAGS
81
+ } = require('./constants');
82
+
83
+ const {
84
+ TestNotFoundError,
85
+ NoAvailableQueueError,
86
+ ClientServerVersionMismatchError
87
+ } = require('./errors');
88
+
89
+ let IS_FARGATE = false;
90
+
91
+ const TEST_RUN_STATUS = require('./test-run-status');
92
+ const prepareTestExecutionPlan = require('../../../util/prepare-test-execution-plan');
93
+ const { PutObjectCommand } = require('@aws-sdk/client-s3');
94
+ const awsGetDefaultRegion = require('../../aws/aws-get-default-region');
95
+
96
+ function setupConsoleReporter(quiet) {
97
+ const reporterOpts = {
98
+ outputFormat: 'classic',
99
+ printPeriod: false,
100
+ quiet: quiet
101
+ };
102
+
103
+ if (
104
+ global.artillery?.version?.startsWith('2')
105
+ ) {
106
+ delete reporterOpts.outputFormat;
107
+ delete reporterOpts.printPeriod;
108
+ }
109
+
110
+ const reporterEvents = new EventEmitter();
111
+ consoleReporter = global.artillery.__createReporter(
112
+ reporterEvents,
113
+ reporterOpts
114
+ );
115
+
116
+ // // Disable spinner on v1
117
+ if (
118
+ global.artillery?.version &&
119
+ !global.artillery.version.startsWith('2')
120
+ ) {
121
+ consoleReporter.spinner.stop();
122
+ consoleReporter.spinner.clear();
123
+ consoleReporter.spinner = {
124
+ start: () => {},
125
+ stop: () => {},
126
+ clear: () => {}
127
+ };
128
+ }
129
+
130
+ return {
131
+ reporterEvents
132
+ };
133
+ }
134
+
135
+ function runCluster(scriptPath, options) {
136
+ const artilleryReporter = setupConsoleReporter(options.quiet);
137
+
138
+ // camelCase all flag names, e.g. `launch-config` becomes launchConfig
139
+ const options2 = {};
140
+ for (const [k, v] of Object.entries(options)) {
141
+ options2[_.camelCase(k)] = v;
142
+ }
143
+ tryRunCluster(scriptPath, options2, artilleryReporter);
144
+ }
145
+
146
+ function logProgress(msg, opts = {}) {
147
+ if (typeof opts.showTimestamp === 'undefined') {
148
+ opts.showTimestamp = true;
149
+ }
150
+ if (global.artillery?.log) {
151
+ artillery.logger(opts).log(msg);
152
+ } else {
153
+ consoleReporter.toggleSpinner();
154
+ artillery.log(
155
+ `${msg} ${chalk.gray(`[${moment().format('HH:mm:ss')}]`)}`
156
+ );
157
+ consoleReporter.toggleSpinner();
158
+ }
159
+ }
160
+
161
+ async function tryRunCluster(scriptPath, options, artilleryReporter) {
162
+ global.artillery.awsRegion = (await awsGetDefaultRegion()) || options.region;
163
+
164
+ let context = {};
165
+ const inputFiles = [].concat(scriptPath, options.config || []);
166
+ const runnableScript = await prepareTestExecutionPlan(inputFiles, options);
167
+
168
+ context.runnableScript = runnableScript;
169
+
170
+ let absoluteScriptPath;
171
+ if (typeof scriptPath !== 'undefined') {
172
+ absoluteScriptPath = path.resolve(process.cwd(), scriptPath);
173
+ context.namedTest = false;
174
+
175
+ try {
176
+ fs.statSync(absoluteScriptPath);
177
+ } catch (_statErr) {
178
+ artillery.log('Could not read file:', scriptPath);
179
+ process.exit(1);
180
+ }
181
+ }
182
+
183
+ if (options.dotenv) {
184
+ const dotEnvPath = path.resolve(process.cwd(), options.dotenv);
185
+ const contents = fs.readFileSync(dotEnvPath);
186
+ context.dotenv = dotenv.parse(contents);
187
+ }
188
+
189
+ const cloudKey = options.key || process.env.ARTILLERY_CLOUD_API_KEY;
190
+ if (cloudKey) {
191
+ const cloudEndpoint = process.env.ARTILLERY_CLOUD_ENDPOINT;
192
+ // Explicitly make Artillery Cloud API key available to workers (if it's set)
193
+ // Relying on the fact that contents of context.dotenv gets passed onto workers
194
+ // for it
195
+ context.dotenv = {
196
+ ...context.dotenv,
197
+ ARTILLERY_CLOUD_API_KEY: cloudKey
198
+ };
199
+
200
+ // Explicitly make Artillery Cloud endpoint available to workers (if it's set)
201
+ if (cloudEndpoint) {
202
+ context.dotenv = {
203
+ ...context.dotenv,
204
+ ARTILLERY_CLOUD_ENDPOINT: cloudEndpoint
205
+ };
206
+ }
207
+ }
208
+
209
+ if (options.bundle) {
210
+ context.namedTest = true;
211
+ }
212
+
213
+ if (options.maxDuration) {
214
+ const maxDurationMs = timeStringToMs(options.maxDuration);
215
+ context.maxDurationMs = maxDurationMs;
216
+ }
217
+
218
+ context.tags = parseTags(options.tags);
219
+
220
+ if (context.tags.length > TEST_RUNS_MAX_TAGS) {
221
+ console.error(
222
+ chalk.red(
223
+ `A maximum of ${TEST_RUNS_MAX_TAGS} tags is allowed per test run`
224
+ )
225
+ );
226
+
227
+ process.exit(1);
228
+ }
229
+
230
+ // Set name tag if not already set:
231
+ if (context.tags.filter((t) => t.name === 'name').length === 0) {
232
+ if (typeof scriptPath !== 'undefined') {
233
+ context.tags.push({
234
+ name: 'name',
235
+ value: path.basename(scriptPath)
236
+ });
237
+ } else {
238
+ context.tags.push({
239
+ name: 'name',
240
+ value: options.bundle
241
+ });
242
+ }
243
+ }
244
+
245
+ if (options.name) {
246
+ for (const t of context.tags) {
247
+ if (t.name === 'name') {
248
+ t.value = options.name;
249
+ }
250
+ }
251
+ }
252
+
253
+ context.extraSecrets = options.secret || [];
254
+
255
+ context.testId = global.artillery.testRunId;
256
+
257
+ if (context.namedTest) {
258
+ context.s3Prefix = options.bundle;
259
+ debug(`Trying to run a named test: ${context.s3Prefix}`);
260
+ }
261
+
262
+ if (!context.namedTest) {
263
+ const contextPath = options.context
264
+ ? path.resolve(options.context)
265
+ : path.dirname(absoluteScriptPath);
266
+
267
+ debugVerbose('script:', absoluteScriptPath);
268
+ debugVerbose('root:', contextPath);
269
+
270
+ const containerScriptPath = path.join(
271
+ path.relative(contextPath, path.dirname(absoluteScriptPath)),
272
+ path.basename(absoluteScriptPath)
273
+ );
274
+
275
+ if (containerScriptPath.indexOf('..') !== -1) {
276
+ artillery.log(
277
+ chalk.red(
278
+ 'Test script must reside inside the context dir. See Artillery docs for more details.'
279
+ )
280
+ );
281
+ process.exit(1);
282
+ }
283
+
284
+ // FIXME: These need clearer names. dir vs path and local vs container.
285
+ context.contextDir = contextPath;
286
+ context.newScriptPath = containerScriptPath;
287
+
288
+ debug('container script path:', containerScriptPath);
289
+ }
290
+
291
+ const count = Number(options.count) || 1;
292
+
293
+ if (typeof options.taskRoleName !== 'undefined') {
294
+ let customRoleName = options.taskRoleName;
295
+ // Allow ARNs for convenience
296
+ // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html
297
+ // We split by :role because role names may contain slash characters (subpaths)
298
+ if (
299
+ customRoleName.startsWith('arn:aws:iam') ||
300
+ customRoleName.startsWith('arn:aws-cn:iam')
301
+ ) {
302
+ customRoleName = customRoleName.split(':role/')[1];
303
+ }
304
+ context.customTaskRoleName = customRoleName;
305
+ }
306
+
307
+ const clusterName = options.cluster || ARTILLERY_CLUSTER_NAME;
308
+ if (options.launchConfig) {
309
+ let launchConfig;
310
+ try {
311
+ launchConfig = JSON.parse(options.launchConfig);
312
+ } catch (parseErr) {
313
+ debug(parseErr);
314
+ }
315
+
316
+ if (!launchConfig) {
317
+ artillery.log(
318
+ chalk.red(
319
+ "Launch config could not be parsed. Please check that it's valid JSON."
320
+ )
321
+ );
322
+ process.exit(1);
323
+ }
324
+
325
+ if (launchConfig.ulimits && !Array.isArray(launchConfig.ulimits)) {
326
+ // TODO: Proper schema validation for the object
327
+ artillery.log(chalk.red('ulimits must be an array of objects'));
328
+ artillery.log(
329
+ 'Please see AWS documentation for more information:\nhttps://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Ulimit.html'
330
+ );
331
+ process.exit(1);
332
+ }
333
+ options.launchConfig = launchConfig;
334
+ } else {
335
+ options.launchConfig = {};
336
+ }
337
+
338
+ if (options.cpu) {
339
+ const n = Number(options.cpu);
340
+ if (Number.isNaN(n)) {
341
+ artillery.log('The value of --cpu must be a number');
342
+ process.exit(1);
343
+ }
344
+
345
+ // Allow specifying 16 vCPU as either "16" or "16384". The actual value is
346
+ // validated later.
347
+ const MAX_VCPUS = 16;
348
+ if (n <= MAX_VCPUS) {
349
+ options.launchConfig.cpu = n * 1024;
350
+ } else {
351
+ options.launchConfig.cpu = n;
352
+ }
353
+ }
354
+
355
+ if (options.memory) {
356
+ const n = Number(options.memory);
357
+ if (Number.isNaN(n)) {
358
+ artillery.log('The value of --memory must be a number');
359
+ process.exit(1);
360
+ }
361
+
362
+ const MAX_MEMORY_IN_GB = 120;
363
+ if (n <= MAX_MEMORY_IN_GB) {
364
+ options.launchConfig.memory = String(parseInt(options.memory, 10) * 1024);
365
+ } else {
366
+ options.launchConfig.memory = options.memory;
367
+ }
368
+ }
369
+
370
+ // check launch type is valid:
371
+ if (typeof options.launchType !== 'undefined') {
372
+ if (
373
+ options.launchType !== 'ecs:fargate' &&
374
+ options.launchType !== 'ecs:ec2'
375
+ ) {
376
+ artillery.log(
377
+ 'Invalid launch type - the value of --launch-type needs to be ecs:fargate or ecs:ec2'
378
+ );
379
+ process.exit(1);
380
+ }
381
+ }
382
+
383
+ if (typeof options.fargate !== 'undefined') {
384
+ console.error(
385
+ 'The --fargate flag is deprecated, use --launch-type ecs:fargate instead'
386
+ );
387
+ }
388
+
389
+ if (options.fargate && options.launchType) {
390
+ console.error(
391
+ 'Either --fargate or --launch-type flag should be set, not both'
392
+ );
393
+ process.exit(1);
394
+ }
395
+
396
+ if (
397
+ typeof options.fargate === 'undefined' &&
398
+ typeof options.launchType === 'undefined'
399
+ ) {
400
+ options.launchType = 'ecs:fargate';
401
+ }
402
+
403
+ IS_FARGATE =
404
+ typeof options.fargate !== 'undefined' || // --fargate set
405
+ typeof options.publicSubnetIds !== 'undefined' || // --public-subnet-ids set
406
+ (typeof options.launchType !== 'undefined' &&
407
+ options.launchType === 'ecs:fargate') || // --launch-type ecs:fargate
408
+ typeof options.launchType === 'undefined';
409
+
410
+ global.artillery.globalEvents.emit('test:init', {
411
+ flags: options,
412
+ testRunId: context.testId,
413
+ tags: context.tags,
414
+ metadata: {
415
+ testId: context.testId,
416
+ startedAt: Date.now(),
417
+ count,
418
+ tags: context.tags,
419
+ launchType: options.launchType
420
+ }
421
+ });
422
+
423
+ let packageJsonPath;
424
+ if (options.packages) {
425
+ packageJsonPath = path.resolve(process.cwd(), options.packages);
426
+ try {
427
+ // TODO: Check that filename is package.json
428
+
429
+ JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
430
+ } catch (err) {
431
+ console.error('Could not load package dependency list');
432
+ console.error('Trying to read from:', packageJsonPath);
433
+ console.error(err);
434
+ }
435
+ }
436
+
437
+ context = Object.assign(context, {
438
+ scriptPath: absoluteScriptPath,
439
+ originalScriptPath: scriptPath,
440
+ count: count,
441
+ region: options.region,
442
+ arnPrefix: options.region.startsWith('cn-') ? 'arn:aws-cn' : 'arn:aws',
443
+ taskName: `${TASK_NAME}_${
444
+ IS_FARGATE ? 'fargate' : ''
445
+ }_${clusterName}_${IMAGE_VERSION.replace(/\./g, '-')}_${Math.floor(
446
+ Math.random() * 1e6
447
+ )}`,
448
+ clusterName: clusterName,
449
+ logGroupName: LOGGROUP_NAME,
450
+ cliOptions: options,
451
+ isFargate: IS_FARGATE,
452
+ isCapacitySpot: typeof options.spot !== 'undefined',
453
+ configTableName: '',
454
+ status: TEST_RUN_STATUS.INITIALIZING,
455
+ packageJsonPath,
456
+ taskArns: []
457
+ });
458
+
459
+ let subnetIds = [];
460
+ if (options.publicSubnetIds) {
461
+ console.error(
462
+ `${chalk.yellow(
463
+ 'Warning'
464
+ )}: --public-subnet-ids will be deprecated. Use --subnet-ids instead.`
465
+ );
466
+
467
+ subnetIds = options.publicSubnetIds.split(',');
468
+ }
469
+ if (options.subnetIds) {
470
+ subnetIds = options.subnetIds.split(',');
471
+ }
472
+
473
+ if (IS_FARGATE) {
474
+ context.fargatePublicSubnetIds = subnetIds;
475
+ context.fargateSecurityGroupIds =
476
+ typeof options.securityGroupIds !== 'undefined'
477
+ ? options.securityGroupIds.split(',')
478
+ : [];
479
+ }
480
+
481
+ if (global.artillery?.telemetry) {
482
+ global.artillery.telemetry.capture('run-test', {
483
+ version: global.artillery.version,
484
+ proVersion: pkg.version,
485
+ count: count,
486
+ launchPlatform: IS_FARGATE ? 'ecs:fargate' : 'ecs:ec2',
487
+ usesTags: context.tags.length > 0,
488
+ region: context.region,
489
+ crossRegion: context.region !== context.backendRegion
490
+ });
491
+ }
492
+
493
+ async function newWaterfall(artilleryReporter) {
494
+ let testRunCompletedSuccessfully = true;
495
+
496
+ let shuttingDown = false;
497
+
498
+ async function gracefulShutdown(opts = { earlyStop: false, exitCode: 0 }) {
499
+ if (shuttingDown) {
500
+ return;
501
+ }
502
+
503
+ shuttingDown = true;
504
+
505
+ if (opts.earlyStop) {
506
+ if (context.status !== TEST_RUN_STATUS.ERROR) {
507
+ // Retain ERROR status if already set elsewhere
508
+ context.status = TEST_RUN_STATUS.EARLY_STOP;
509
+ }
510
+ }
511
+ await cleanupResources(context);
512
+
513
+ global.artillery.globalEvents.emit('shutdown:start', {
514
+ exitCode: opts.exitCode,
515
+ earlyStop: opts.earlyStop
516
+ });
517
+
518
+ const ps = [];
519
+ for (const e of global.artillery.extensionEvents) {
520
+ const testInfo = { endTime: Date.now() };
521
+ if (e.ext === 'beforeExit') {
522
+ ps.push(
523
+ e.method({
524
+ report: context.aggregateReport,
525
+ flags: context.cliOptions,
526
+ runnerOpts: {
527
+ environment: context.cliOptions?.environment,
528
+ scriptPath: '',
529
+ absoluteScriptPath: ''
530
+ },
531
+ testInfo
532
+ })
533
+ );
534
+ }
535
+ }
536
+ await Promise.allSettled(ps);
537
+
538
+ const ps2 = [];
539
+ const shutdownOpts = {
540
+ earlyStop: opts.earlyStop,
541
+ exitCode: opts.exitCode
542
+ };
543
+ for (const e of global.artillery.extensionEvents) {
544
+ if (e.ext === 'onShutdown') {
545
+ ps2.push(e.method(shutdownOpts));
546
+ }
547
+ }
548
+ await Promise.allSettled(ps2);
549
+
550
+ process.exit(global.artillery.suggestedExitCode || opts.exitCode);
551
+ }
552
+
553
+ global.artillery.shutdown = gracefulShutdown;
554
+
555
+ process.on('SIGINT', async () => {
556
+ if (shuttingDown) {
557
+ return;
558
+ }
559
+ console.log('Stopping test run (SIGINT received)...');
560
+ await gracefulShutdown({ exitCode: 1, earlyStop: true });
561
+ });
562
+ process.on('SIGTERM', async () => {
563
+ if (shuttingDown) {
564
+ return;
565
+ }
566
+ console.log('Stopping test run (SIGTERM received)...');
567
+ await gracefulShutdown({ exitCode: 1, earlyStop: true });
568
+ });
569
+
570
+ // Messages from SQS reporter created later will be relayed via this EE
571
+ context.reporterEvents = artilleryReporter.reporterEvents;
572
+
573
+ try {
574
+ logProgress('Checking AWS connectivity...');
575
+ context.accountId = await getAccountId({ region: context.region });
576
+ await Promise.all([
577
+ (async (context) => {
578
+ const bucketName = await getBucketName();
579
+ context.s3Bucket = bucketName;
580
+ return context;
581
+ })(context)
582
+ ]);
583
+
584
+ logProgress('Checking cluster...');
585
+ const clusterExists = await checkTargetCluster(context);
586
+
587
+ if (!clusterExists) {
588
+ if (typeof context.cliOptions.cluster === 'undefined') {
589
+ // User did not specify a cluster with --cluster, and ARTILLERY_CLUSTER_NAME
590
+ // does not exist, so create it
591
+ await createArtilleryCluster(context);
592
+ } else {
593
+ // User specified a cluster, but it's not there
594
+ throw new Error(
595
+ `Could not find cluster ${context.clusterName} in ${context.region}`
596
+ );
597
+ }
598
+ }
599
+
600
+ if (context.tags.length > 0) {
601
+ logProgress(
602
+ `Tags: ${context.tags.map((t) => `${t.name}:${t.value}`).join(', ')}`
603
+ );
604
+ }
605
+ logProgress(`Test run ID: ${context.testId}`);
606
+
607
+ logProgress('Preparing launch platform...');
608
+
609
+ await maybeGetSubnetIdsForFargate(context);
610
+
611
+ logProgress(
612
+ `Environment:
613
+ Account: ${context.accountId}
614
+ Region: ${context.region}
615
+ Count: ${context.count}
616
+ Cluster: ${context.clusterName}
617
+ Launch type: ${context.cliOptions.launchType} ${
618
+ context.isFargate && context.isCapacitySpot ? '(Spot)' : '(On-demand)'
619
+ }
620
+ `,
621
+ { showTimestamp: false }
622
+ );
623
+
624
+ await createQueue(context);
625
+ await checkCustomTaskRole(context);
626
+ logProgress('Preparing test bundle...');
627
+ await createTestBundle(context);
628
+ await createADOTDefinitionIfNeeded(context);
629
+ await ensureTaskExists(context);
630
+ await getManifest(context);
631
+ await generateTaskOverrides(context);
632
+
633
+ logProgress('Launching workers...');
634
+ await setupDefaultECSParams(context);
635
+
636
+ if (
637
+ context.status !== TEST_RUN_STATUS.EARLY_STOP &&
638
+ context.status !== TEST_RUN_STATUS.TERMINATING
639
+ ) {
640
+ // Set up SQS listener:
641
+ listen(context, artilleryReporter.reporterEvents);
642
+ await launchLeadTask(context);
643
+ }
644
+
645
+ setCloudwatchRetention(
646
+ `${LOGGROUP_NAME}/${context.clusterName}`,
647
+ LOGGROUP_RETENTION_DAYS,
648
+ context.region,
649
+ {
650
+ maxRetries: 10,
651
+ waitPerRetry: 2 * 1000
652
+ }
653
+ );
654
+
655
+ if (
656
+ context.status !== TEST_RUN_STATUS.EARLY_STOP &&
657
+ context.status !== TEST_RUN_STATUS.TERMINATING
658
+ ) {
659
+ logProgress(
660
+ context.isFargate ? 'Waiting for Fargate...' : 'Waiting for ECS...'
661
+ );
662
+ await ecsRunTask(context);
663
+ }
664
+
665
+ if (
666
+ context.status !== TEST_RUN_STATUS.EARLY_STOP &&
667
+ context.status !== TEST_RUN_STATUS.TERMINATING
668
+ ) {
669
+ await waitForTasks2(context);
670
+ }
671
+
672
+ if (
673
+ context.status !== TEST_RUN_STATUS.EARLY_STOP &&
674
+ context.status !== TEST_RUN_STATUS.TERMINATING
675
+ ) {
676
+ logProgress('Waiting for workers to come online...');
677
+ await waitForWorkerSync(context);
678
+ await sendGoSignal(context);
679
+ logProgress('Workers are running, waiting for reports...');
680
+
681
+ if (context.maxDurationMs && context.maxDurationMs > 0) {
682
+ logProgress(
683
+ `Max duration for test run is set to: ${context.cliOptions.maxDuration}`
684
+ );
685
+ const testDurationTimeout = new Timeout(context.maxDurationMs);
686
+ testDurationTimeout.start();
687
+ testDurationTimeout.on('timeout', async () => {
688
+ artillery.log(
689
+ `Max duration of test run exceeded: ${context.cliOptions.maxDuration}\n`
690
+ );
691
+ await gracefulShutdown({ earlyStop: true });
692
+ });
693
+ }
694
+
695
+ context.status = TEST_RUN_STATUS.RECEIVING_REPORTS;
696
+ }
697
+
698
+ // Need to wait for all reports to be over here, not exit
699
+ const workerState = await awaitOnEE(
700
+ artilleryReporter.reporterEvents,
701
+ 'workersDone'
702
+ );
703
+ debug(workerState);
704
+
705
+ logProgress(`Test run completed: ${context.testId}`);
706
+
707
+ context.status = TEST_RUN_STATUS.COMPLETED;
708
+
709
+ let checks = [];
710
+ global.artillery.globalEvents.once('checks', async (results) => {
711
+ checks = results;
712
+ });
713
+
714
+ if (context.ensureSpec) {
715
+ new EnsurePlugin.Plugin({ config: { ensure: context.ensureSpec } });
716
+ }
717
+
718
+ if (context.fullyResolvedConfig?.plugins?.slack) {
719
+ new SlackPlugin.Plugin({
720
+ config: context.fullyResolvedConfig
721
+ });
722
+ }
723
+
724
+ if (context.cliOptions.output) {
725
+ const logfile = getLogFilename(
726
+ context.cliOptions.output,
727
+ defaultOptions.logFilenameFormat
728
+ );
729
+
730
+ for (const ix of context.intermediateReports) {
731
+ delete ix.histograms;
732
+ ix.histograms = ix.summaries;
733
+ }
734
+ delete context.aggregateReport.histograms;
735
+ context.aggregateReport.histograms = context.aggregateReport.summaries;
736
+
737
+ const jsonReport = {
738
+ intermediate: context.intermediateReports,
739
+ aggregate: context.aggregateReport,
740
+ testId: context.testId,
741
+ metadata: {
742
+ tags: context.tags,
743
+ count: context.count,
744
+ region: context.region,
745
+ cluster: context.clusterName,
746
+ artilleryVersion: {
747
+ core: global.artillery.version,
748
+ pro: pkg.version
749
+ }
750
+ },
751
+ ensure: checks.map((c) => {
752
+ return {
753
+ condition: c.original,
754
+ success: c.result === 1,
755
+ strict: c.strict
756
+ };
757
+ })
758
+ };
759
+
760
+ fs.writeFileSync(logfile, JSON.stringify(jsonReport, null, 2), {
761
+ flag: 'w'
762
+ });
763
+ }
764
+ debug(context.testId, 'done');
765
+ } catch (err) {
766
+ debug(err);
767
+ if (err instanceof InvalidParameterException) {
768
+ if (
769
+ err.message
770
+ .toLowerCase()
771
+ .indexOf('no container instances were found') !== -1
772
+ ) {
773
+ artillery.log(
774
+ chalk.yellow('The ECS cluster has no active EC2 instances')
775
+ );
776
+ } else {
777
+ artillery.log(err);
778
+ }
779
+ } else if (err instanceof TestNotFoundError) {
780
+ artillery.log(`Test ${context.s3Prefix} not found`);
781
+ } else if (
782
+ err instanceof NoAvailableQueueError ||
783
+ err instanceof ClientServerVersionMismatchError
784
+ ) {
785
+ artillery.log(chalk.red('Error:', err.message));
786
+ } else {
787
+ artillery.log(util.formatError(err));
788
+ artillery.log(err);
789
+ artillery.log(err.stack);
790
+ }
791
+ testRunCompletedSuccessfully = false;
792
+ global.artillery.suggestedExitCode = 1;
793
+ } finally {
794
+ if (!testRunCompletedSuccessfully) {
795
+ logProgress('Cleaning up...');
796
+ context.status = TEST_RUN_STATUS.ERROR;
797
+ await gracefulShutdown({ earlyStop: true, exitCode: 1 });
798
+ } else {
799
+ context.status = TEST_RUN_STATUS.COMPLETED;
800
+ await gracefulShutdown({ earlyStop: false, exitCode: 0 });
801
+ }
802
+ }
803
+ }
804
+
805
+ await newWaterfall(artilleryReporter);
806
+ }
807
+
808
+ async function cleanupResources(context) {
809
+ try {
810
+ if (context.sqsReporter) {
811
+ context.sqsReporter.stop();
812
+ }
813
+
814
+ if (context.adot?.SSMParameterPath) {
815
+ await awsUtil.deleteParameter(
816
+ context.adot.SSMParameterPath,
817
+ context.region
818
+ );
819
+ }
820
+
821
+ if (context.taskArns && context.taskArns.length > 0) {
822
+ for (const taskArn of context.taskArns) {
823
+ try {
824
+ const ecs = new ECSClient({
825
+ apiVersion: '2014-11-13',
826
+ region: context.region
827
+ });
828
+ await ecs.send(
829
+ new StopTaskCommand({
830
+ task: taskArn,
831
+ cluster: context.clusterName,
832
+ reason: 'Test cleanup'
833
+ })
834
+ );
835
+ } catch (err) {
836
+ // TODO: Retry if appropriate, give the user more information
837
+ // to be able to fall back to manual intervention if possible.
838
+ // TODO: Consumer has no idea if this succeeded or not
839
+ debug(err);
840
+ }
841
+ }
842
+ }
843
+
844
+ // TODO: Should either retry, or not throw in any of these
845
+ await Promise.all([
846
+ deleteQueue(context),
847
+ deregisterTaskDefinition(context),
848
+ gcQueues(context)
849
+ ]);
850
+ } catch (err) {
851
+ artillery.log(err);
852
+ }
853
+ }
854
+
855
+ function checkFargateResourceConfig(cpu, memory) {
856
+ function generateListOfOptionsMiB(minGB, maxGB, incrementGB) {
857
+ const result = [];
858
+ for (let i = 0; i <= (maxGB - minGB) / incrementGB; i++) {
859
+ result.push((minGB + incrementGB * i) * 1024);
860
+ }
861
+
862
+ return result;
863
+ }
864
+
865
+ // Based on https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html
866
+ const FARGATE_VALID_CONFIGS = {
867
+ 256: [512, 1024, 2048],
868
+ 512: [1024, 2048, 3072, 4096],
869
+ 1024: [2048, 3072, 4096, 5120, 6144, 7168, 8192],
870
+ 2048: generateListOfOptionsMiB(4, 16, 1),
871
+ 4096: generateListOfOptionsMiB(8, 30, 1),
872
+ 8192: generateListOfOptionsMiB(16, 60, 4),
873
+ 16384: generateListOfOptionsMiB(32, 120, 8)
874
+ };
875
+
876
+ if (!FARGATE_VALID_CONFIGS[cpu]) {
877
+ return new Error(
878
+ `Unsupported cpu override for Fargate. Must be one of: ${Object.keys(
879
+ FARGATE_VALID_CONFIGS
880
+ ).join(', ')}`
881
+ );
882
+ }
883
+
884
+ if (FARGATE_VALID_CONFIGS[cpu].indexOf(memory) < 0) {
885
+ return new Error(
886
+ `Fargate memory override for cpu = ${cpu} must be one of: ${FARGATE_VALID_CONFIGS[
887
+ cpu
888
+ ].join(', ')}`
889
+ );
890
+ }
891
+
892
+ return null;
893
+ }
894
+
895
+ async function createArtilleryCluster(context) {
896
+ const ecs = new ECSClient({
897
+ apiVersion: '2014-11-13',
898
+ region: context.region
899
+ });
900
+ await ecs.send(
901
+ new CreateClusterCommand({
902
+ clusterName: ARTILLERY_CLUSTER_NAME,
903
+ capacityProviders: ['FARGATE_SPOT']
904
+ })
905
+ );
906
+
907
+ let retries = 0;
908
+ while (retries < 12) {
909
+ const clusterActive = await checkTargetCluster(context);
910
+ if (clusterActive) {
911
+ break;
912
+ }
913
+ retries++;
914
+ await sleep(10 * 1000);
915
+ }
916
+ }
917
+
918
+ //
919
+ // Check that ECS cluster exists:
920
+ //
921
+ async function checkTargetCluster(context) {
922
+ const ecs = new ECSClient({
923
+ apiVersion: '2014-11-13',
924
+ region: context.region
925
+ });
926
+ try {
927
+ const response = await ecs.send(
928
+ new DescribeClustersCommand({ clusters: [context.clusterName] })
929
+ );
930
+ debug(response);
931
+ if (response.clusters.length === 0 || response.failures.length > 0) {
932
+ debugVerbose(response);
933
+ return false;
934
+ } else {
935
+ const activeClusters = response.clusters.filter(
936
+ (c) => c.status === 'ACTIVE'
937
+ );
938
+ return activeClusters.length > 0;
939
+ }
940
+ } catch (err) {
941
+ debugVerbose(err);
942
+ return false;
943
+ }
944
+ }
945
+
946
+ async function maybeGetSubnetIdsForFargate(context) {
947
+ if (!context.isFargate) {
948
+ return context;
949
+ }
950
+
951
+ // TODO: Sanity check that subnets actually exist before trying to use them in test definitions
952
+
953
+ if (context.fargatePublicSubnetIds.length > 0) {
954
+ return context;
955
+ }
956
+
957
+ debug('Subnet IDs not provided, looking up default VPC');
958
+
959
+ const f = new VPCSubnetFinder({ region: context.region });
960
+ const publicSubnets = await f.findPublicSubnets();
961
+
962
+ if (publicSubnets.length === 0) {
963
+ throw new Error('Could not find public subnets in default VPC');
964
+ }
965
+
966
+ context.fargatePublicSubnetIds = publicSubnets.map((s) => s.SubnetId);
967
+
968
+ debug('Found public subnets:', context.fargatePublicSubnetIds.join(', '));
969
+
970
+ return context;
971
+ }
972
+
973
+ async function createTestBundle(context) {
974
+ const result = await createTest(context.scriptPath, {
975
+ name: context.testId,
976
+ config: context.cliOptions.config,
977
+ packageJsonPath: context.packageJsonPath,
978
+ flags: context.cliOptions
979
+ });
980
+
981
+ context.fullyResolvedConfig = result.manifest.fullyResolvedConfig;
982
+
983
+ return context;
984
+ }
985
+
986
+ async function createADOTDefinitionIfNeeded(context) {
987
+ const publishMetricsConfig =
988
+ context.fullyResolvedConfig.plugins?.['publish-metrics'];
989
+ if (!publishMetricsConfig) {
990
+ debug('No publish-metrics plugin set, skipping ADOT configuration');
991
+ return context;
992
+ }
993
+
994
+ const adotRelevantConfigs =
995
+ getADOTRelevantReporterConfigs(publishMetricsConfig);
996
+ if (adotRelevantConfigs.length === 0) {
997
+ debug('No ADOT relevant reporter configs set, skipping ADOT configuration');
998
+ return context;
999
+ }
1000
+
1001
+ try {
1002
+ const { adotEnvVars, adotConfig } = resolveADOTConfigSettings({
1003
+ configList: adotRelevantConfigs,
1004
+ dotenv: { ...context.dotenv }
1005
+ });
1006
+
1007
+ context.dotenv = Object.assign(context.dotenv || {}, adotEnvVars);
1008
+
1009
+ context.adot = {
1010
+ SSMParameterPath: `/artilleryio/OTEL_CONFIG_${context.testId}`
1011
+ };
1012
+
1013
+ await awsUtil.putParameter(
1014
+ context.adot.SSMParameterPath,
1015
+ JSON.stringify(adotConfig),
1016
+ 'String',
1017
+ context.region
1018
+ );
1019
+
1020
+ context.adot.taskDefinition = {
1021
+ name: 'adot-collector',
1022
+ image: 'amazon/aws-otel-collector:v0.39.0',
1023
+ command: [
1024
+ '--config=/etc/ecs/container-insights/otel-task-metrics-config.yaml'
1025
+ ],
1026
+ secrets: [
1027
+ {
1028
+ name: 'AOT_CONFIG_CONTENT',
1029
+ valueFrom: `${context.arnPrefix}:ssm:${context.region}:${context.accountId}:parameter${context.adot.SSMParameterPath}`
1030
+ }
1031
+ ],
1032
+ logConfiguration: {
1033
+ logDriver: 'awslogs',
1034
+ options: {
1035
+ 'awslogs-group': `${context.logGroupName}/${context.clusterName}`,
1036
+ 'awslogs-region': context.region,
1037
+ 'awslogs-stream-prefix': `artilleryio/${context.testId}`,
1038
+ 'awslogs-create-group': 'true'
1039
+ }
1040
+ }
1041
+ };
1042
+ } catch (err) {
1043
+ throw new Error(err);
1044
+ }
1045
+ return context;
1046
+ }
1047
+
1048
+ async function ensureTaskExists(context) {
1049
+ const ecs = new ECSClient({
1050
+ apiVersion: '2014-11-13',
1051
+ region: context.region
1052
+ });
1053
+
1054
+ // Note: these are integers for container definitions, and strings for task definitions (on Fargate)
1055
+ // Defaults have to be Fargate-compatible
1056
+ // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size
1057
+ let cpu = 4096;
1058
+ let memory = 8192;
1059
+
1060
+ const defaultUlimits = {
1061
+ nofile: {
1062
+ softLimit: 8192,
1063
+ hardLimit: 8192
1064
+ }
1065
+ };
1066
+ let ulimits = [];
1067
+
1068
+ if (context.cliOptions.launchConfig) {
1069
+ const lc = context.cliOptions.launchConfig;
1070
+ if (lc.cpu) {
1071
+ cpu = parseInt(lc.cpu, 10);
1072
+ }
1073
+ if (lc.memory) {
1074
+ memory = parseInt(lc.memory, 10);
1075
+ }
1076
+
1077
+ if (lc.ulimits) {
1078
+ lc.ulimits.forEach((u) => {
1079
+ if (!defaultUlimits[u.name]) {
1080
+ defaultUlimits[u.name] = {};
1081
+ }
1082
+ defaultUlimits[u.name] = {
1083
+ softLimit: u.softLimit,
1084
+ hardLimit: typeof u.hardLimit === 'number' ? u.hardLimit : u.softLimit
1085
+ };
1086
+ });
1087
+ }
1088
+
1089
+ // TODO: Check this earlier to return an error faster.
1090
+ if (context.isFargate) {
1091
+ const configErr = checkFargateResourceConfig(cpu, memory);
1092
+ if (configErr) {
1093
+ throw configErr;
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ ulimits = Object.keys(defaultUlimits).map((name) => {
1099
+ return {
1100
+ name: name,
1101
+ softLimit: defaultUlimits[name].softLimit,
1102
+ hardLimit: defaultUlimits[name].hardLimit
1103
+ };
1104
+ });
1105
+
1106
+ const defaultArchitecture = 'x86_64';
1107
+ const imageUrl =
1108
+ process.env.WORKER_IMAGE_URL ||
1109
+ `public.ecr.aws/d8a4z9o5/artillery-worker:${IMAGE_VERSION}-${defaultArchitecture}`;
1110
+
1111
+ const secrets = [
1112
+ 'NPM_TOKEN',
1113
+ 'NPM_REGISTRY',
1114
+ 'NPM_SCOPE',
1115
+ 'NPM_SCOPE_REGISTRY',
1116
+ 'NPMRC',
1117
+ 'ARTIFACTORY_AUTH',
1118
+ 'ARTIFACTORY_EMAIL'
1119
+ ]
1120
+ .concat(context.extraSecrets)
1121
+ .map((secretName) => {
1122
+ return {
1123
+ name: secretName,
1124
+ valueFrom: `${context.arnPrefix}:ssm:${context.region}:${context.accountId}:parameter/artilleryio/${secretName}`
1125
+ };
1126
+ });
1127
+
1128
+ const artilleryContainerDefinition = {
1129
+ name: 'artillery',
1130
+ image: imageUrl,
1131
+ cpu: cpu,
1132
+ command: [],
1133
+ entryPoint: ['/artillery/loadgen-worker'],
1134
+ memory: memory,
1135
+ secrets: secrets,
1136
+ ulimits: ulimits,
1137
+ essential: true,
1138
+ logConfiguration: {
1139
+ logDriver: 'awslogs',
1140
+ options: {
1141
+ 'awslogs-group': `${context.logGroupName}/${context.clusterName}`,
1142
+ 'awslogs-region': context.region,
1143
+ 'awslogs-stream-prefix': `artilleryio/${context.testId}`,
1144
+ 'awslogs-create-group': 'true',
1145
+ mode: 'non-blocking'
1146
+ }
1147
+ }
1148
+ };
1149
+
1150
+ if (context.cliOptions.containerDnsServers) {
1151
+ artilleryContainerDefinition.dnsServers =
1152
+ context.cliOptions.containerDnsServers.split(',');
1153
+ }
1154
+
1155
+ const taskDefinition = {
1156
+ family: context.taskName,
1157
+ containerDefinitions: [artilleryContainerDefinition],
1158
+ executionRoleArn: context.taskRoleArn
1159
+ };
1160
+
1161
+ if (typeof context.adot !== 'undefined') {
1162
+ taskDefinition.containerDefinitions.push(context.adot.taskDefinition);
1163
+ }
1164
+
1165
+ context.taskDefinition = taskDefinition;
1166
+
1167
+ if (!context.isFargate && taskDefinition.containerDefinitions.length > 1) {
1168
+ // Limits for sidecar have to be set explicitly on ECS EC2
1169
+ taskDefinition.containerDefinitions[1].memory = 1024;
1170
+ taskDefinition.containerDefinitions[1].cpu = 1024;
1171
+ }
1172
+
1173
+ if (context.isFargate) {
1174
+ taskDefinition.networkMode = 'awsvpc';
1175
+ taskDefinition.requiresCompatibilities = ['FARGATE'];
1176
+ taskDefinition.cpu = String(cpu);
1177
+ taskDefinition.memory = String(memory);
1178
+ // NOTE: This role must exist.
1179
+ // This value cannot be an override, meaning it's hardcoded into the task definition.
1180
+ // That in turn means that if the role is updated then the task definition needs to be
1181
+ // recreated too
1182
+ taskDefinition.executionRoleArn = context.taskRoleArn; // TODO: A separate role for Fargate
1183
+ }
1184
+
1185
+ const params = {
1186
+ taskDefinition: context.taskName
1187
+ };
1188
+
1189
+ debug('Task definition\n', JSON.stringify(taskDefinition, null, 4));
1190
+
1191
+ try {
1192
+ await ecs.send(new DescribeTaskDefinitionCommand(params));
1193
+ debug('OK: ECS task exists');
1194
+ if (process.env.ECR_IMAGE_VERSION) {
1195
+ debug(
1196
+ 'ECR_IMAGE_VERSION is set, but the task definition was already in place.'
1197
+ );
1198
+ }
1199
+ return context;
1200
+ } catch (_err) {
1201
+ try {
1202
+ const response = await ecs.send(
1203
+ new RegisterTaskDefinitionCommand(taskDefinition)
1204
+ );
1205
+ debug('OK: ECS task registered');
1206
+ debugVerbose(JSON.stringify(response, null, 4));
1207
+ context.taskDefinitionArn = response.taskDefinition.taskDefinitionArn;
1208
+ debug(`Task definition ARN: ${context.taskDefinitionArn}`);
1209
+ return context;
1210
+ } catch (registerErr) {
1211
+ artillery.log(registerErr);
1212
+ artillery.log('Could not create ECS task, please try again');
1213
+ throw registerErr;
1214
+ }
1215
+ }
1216
+ }
1217
+
1218
+ async function checkCustomTaskRole(context) {
1219
+ if (!context.customTaskRoleName) {
1220
+ return;
1221
+ }
1222
+
1223
+ const iam = new IAMClient({ region: global.artillery.awsRegion });
1224
+ const roleData = await iam.send(
1225
+ new GetRoleCommand({ RoleName: context.customTaskRoleName })
1226
+ );
1227
+ context.customRoleArn = roleData.Role.Arn;
1228
+ context.taskRoleArn = roleData.Role.Arn;
1229
+ debug(roleData);
1230
+ }
1231
+
1232
+ async function gcQueues(context) {
1233
+ const sqs = new SQSClient({
1234
+ region: context.region
1235
+ });
1236
+
1237
+ let data;
1238
+ try {
1239
+ data = await sqs.send(
1240
+ new ListQueuesCommand({
1241
+ QueueNamePrefix: SQS_QUEUES_NAME_PREFIX,
1242
+ MaxResults: 1000
1243
+ })
1244
+ );
1245
+ } catch (err) {
1246
+ debug(err);
1247
+ }
1248
+
1249
+ if (data?.QueueUrls && data.QueueUrls.length > 0) {
1250
+ for (const qu of data.QueueUrls) {
1251
+ try {
1252
+ const data = await sqs.send(
1253
+ new GetQueueAttributesCommand({
1254
+ QueueUrl: qu,
1255
+ AttributeNames: ['CreatedTimestamp']
1256
+ })
1257
+ );
1258
+ const ts = Number(data.Attributes.CreatedTimestamp) * 1000;
1259
+ // Delete after 96 hours
1260
+ if (Date.now() - ts > 96 * 60 * 60 * 1000) {
1261
+ await sqs.send(new DeleteQueueCommand({ QueueUrl: qu }));
1262
+ }
1263
+ } catch (err) {
1264
+ // TODO: Filter on errors which may be ignored, e.g.:
1265
+ // AWS.SimpleQueueService.NonExistentQueue: The specified queue does not exist
1266
+ // which can happen if another test ends between calls to listQueues and getQueueAttributes.
1267
+ // Sometimes SQS returns recently deleted queues to ListQueues too.
1268
+ debug(err);
1269
+ }
1270
+ }
1271
+ }
1272
+ }
1273
+
1274
+ async function deleteQueue(context) {
1275
+ if (!context.sqsQueueUrl) {
1276
+ return;
1277
+ }
1278
+
1279
+ const sqs = new SQSClient({
1280
+ region: context.region
1281
+ });
1282
+
1283
+ try {
1284
+ await sqs.send(new DeleteQueueCommand({ QueueUrl: context.sqsQueueUrl }));
1285
+ } catch (err) {
1286
+ console.error(`Unable to clean up SQS queue. URL: ${context.sqsQueueUrl}`);
1287
+ debug(err);
1288
+ }
1289
+ }
1290
+
1291
+ async function createQueue(context) {
1292
+ const sqs = new SQSClient({
1293
+ region: context.region
1294
+ });
1295
+
1296
+ const queueName = `${SQS_QUEUES_NAME_PREFIX}_${context.testId.slice(
1297
+ 0,
1298
+ 30
1299
+ )}.fifo`;
1300
+ const params = {
1301
+ QueueName: queueName,
1302
+ Attributes: {
1303
+ FifoQueue: 'true',
1304
+ ContentBasedDeduplication: 'false',
1305
+ MessageRetentionPeriod: '1800',
1306
+ VisibilityTimeout: '600' // 10 minutes
1307
+ }
1308
+ };
1309
+ const result = await sqs.send(new CreateQueueCommand(params));
1310
+ context.sqsQueueUrl = result.QueueUrl;
1311
+
1312
+ // Wait for the queue to be available:
1313
+ let waited = 0;
1314
+ let ok = false;
1315
+ while (waited < 120 * 1000) {
1316
+ try {
1317
+ const results = await sqs.send(
1318
+ new ListQueuesCommand({ QueueNamePrefix: queueName })
1319
+ );
1320
+ if (results.QueueUrls && results.QueueUrls.length === 1) {
1321
+ debug('SQS queue created:', queueName);
1322
+ ok = true;
1323
+ break;
1324
+ } else {
1325
+ await sleep(10 * 1000);
1326
+ waited += 10 * 1000;
1327
+ }
1328
+ } catch (_err) {
1329
+ await sleep(10 * 1000);
1330
+ waited += 10 * 1000;
1331
+ }
1332
+ }
1333
+
1334
+ if (!ok) {
1335
+ debug('Time out waiting for SQS queue:', queueName);
1336
+ throw new Error('SQS queue could not be created');
1337
+ }
1338
+ }
1339
+
1340
+ async function getManifest(context) {
1341
+ try {
1342
+ const s3 = createS3Client({ region: global.artillery.s3BucketRegion });
1343
+ const params = {
1344
+ Bucket: context.s3Bucket,
1345
+ Key: `tests/${context.testId}/metadata.json`
1346
+ };
1347
+
1348
+ const data = await s3.send(new GetObjectCommand(params));
1349
+ const metadata = JSON.parse(await data.Body.transformToString());
1350
+ context.newScriptPath = metadata.scriptPath;
1351
+
1352
+ if (metadata.configPath) {
1353
+ context.configPath = metadata.configPath;
1354
+ }
1355
+
1356
+ return context;
1357
+ } catch (err) {
1358
+ if (err instanceof NoSuchKey) {
1359
+ throw new TestNotFoundError();
1360
+ } else {
1361
+ throw err;
1362
+ }
1363
+ }
1364
+ }
1365
+
1366
+ async function generateTaskOverrides(context) {
1367
+ const cliArgs = ['run'].concat(
1368
+ context.cliOptions.environment
1369
+ ? ['--environment', context.cliOptions.environment]
1370
+ : [],
1371
+ context.cliOptions['scenarioName']
1372
+ ? ['--scenario-name', context.cliOptions['scenarioName']]
1373
+ : [],
1374
+ context.cliOptions.insecure ? ['-k'] : [],
1375
+ context.cliOptions.target ? ['-t', context.cliOptions.target] : [],
1376
+ context.cliOptions.overrides
1377
+ ? ['--overrides', context.cliOptions.overrides]
1378
+ : [],
1379
+ context.cliOptions.variables
1380
+ ? ['--variables', context.cliOptions.variables]
1381
+ : [],
1382
+ context.configPath ? ['--config', context.configPath] : []
1383
+ );
1384
+ // NOTE: This MUST come last:
1385
+ cliArgs.push(context.newScriptPath);
1386
+
1387
+ debug('cliArgs', cliArgs, cliArgs.join(' '));
1388
+
1389
+ const s3path = `s3://${context.s3Bucket}/tests/${
1390
+ context.namedTest ? context.s3Prefix : context.testId
1391
+ }`;
1392
+ const adotOverride = [
1393
+ {
1394
+ name: 'adot-collector',
1395
+ environment: []
1396
+ }
1397
+ ];
1398
+
1399
+ const overrides = {
1400
+ containerOverrides: [
1401
+ {
1402
+ name: 'artillery',
1403
+ command: [
1404
+ '-p',
1405
+ s3path,
1406
+ '-a',
1407
+ util.btoa(JSON.stringify(cliArgs)),
1408
+ '-r',
1409
+ context.region,
1410
+ '-q',
1411
+ process.env.SQS_QUEUE_URL || context.sqsQueueUrl,
1412
+ '-i',
1413
+ context.testId,
1414
+ '-d',
1415
+ `s3://${context.s3Bucket}/test-runs`,
1416
+ '-t',
1417
+ String(WAIT_TIMEOUT)
1418
+ ],
1419
+ environment: [
1420
+ {
1421
+ name: 'AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE',
1422
+ value: '1'
1423
+ },
1424
+ {
1425
+ name: 'ARTILLERY_TEST_RUN_ID',
1426
+ value: global.artillery.testRunId
1427
+ },
1428
+ {
1429
+ name: 'ARTILLERY_S3_BUCKET',
1430
+ value: context.s3Bucket
1431
+ }
1432
+ ]
1433
+ },
1434
+ ...(context.adot ? adotOverride : [])
1435
+ ],
1436
+ taskRoleArn: context.taskRoleArn
1437
+ };
1438
+
1439
+ if (context.customRoleArn) {
1440
+ overrides.taskRoleArn = context.customRoleArn;
1441
+ }
1442
+
1443
+ if (context.cliOptions.taskEphemeralStorage) {
1444
+ overrides.ephemeralStorage = {
1445
+ sizeInGiB: context.cliOptions.taskEphemeralStorage
1446
+ };
1447
+ }
1448
+
1449
+ overrides.containerOverrides[0].environment.push({
1450
+ name: 'USE_V2',
1451
+ value: 'true'
1452
+ });
1453
+
1454
+ if (context.dotenv) {
1455
+ const extraEnv = [];
1456
+ for (const [name, value] of Object.entries(context.dotenv)) {
1457
+ extraEnv.push({ name, value });
1458
+ }
1459
+ overrides.containerOverrides[0].environment =
1460
+ overrides.containerOverrides[0].environment.concat(extraEnv);
1461
+ if (overrides.containerOverrides[1]) {
1462
+ overrides.containerOverrides[1].environment =
1463
+ overrides.containerOverrides[1].environment.concat(extraEnv);
1464
+ }
1465
+ }
1466
+
1467
+ if (context.cliOptions.launchConfig) {
1468
+ const lc = context.cliOptions.launchConfig;
1469
+ if (lc.environment) {
1470
+ overrides.containerOverrides[0].environment =
1471
+ overrides.containerOverrides[0].environment.concat(lc.environment);
1472
+ if (overrides.containerOverrides[1]) {
1473
+ overrides.containerOverrides[1].environment =
1474
+ overrides.containerOverrides[1].environment.concat(lc.environment);
1475
+ }
1476
+ }
1477
+
1478
+ //
1479
+ // Not officially supported:
1480
+ //
1481
+ if (lc.taskRoleArn) {
1482
+ overrides.taskRoleArn = lc.taskRoleArn;
1483
+ }
1484
+ if (lc.command) {
1485
+ overrides.containerOverrides[0].command = lc.command;
1486
+ }
1487
+ }
1488
+
1489
+ debug('OK: Overrides generated');
1490
+ debugVerbose(JSON.stringify(overrides, null, 4));
1491
+
1492
+ context.taskOverrides = overrides;
1493
+
1494
+ return context;
1495
+ }
1496
+
1497
+ async function setupDefaultECSParams(context) {
1498
+ const defaultParams = {
1499
+ taskDefinition: context.taskName,
1500
+ cluster: context.clusterName,
1501
+ overrides: context.taskOverrides
1502
+ };
1503
+
1504
+ if (context.isFargate) {
1505
+ if (context.isCapacitySpot) {
1506
+ defaultParams.capacityProviderStrategy = [
1507
+ {
1508
+ capacityProvider: 'FARGATE_SPOT',
1509
+ weight: 1,
1510
+ base: 0
1511
+ }
1512
+ ];
1513
+ } else {
1514
+ // On-demand capacity
1515
+ defaultParams.launchType = 'FARGATE';
1516
+ }
1517
+ // Networking config: private subnets of the VPC that the ECS cluster
1518
+ // is in. Don't need public subnets.
1519
+ defaultParams.networkConfiguration = {
1520
+ awsvpcConfiguration: {
1521
+ // https://github.com/aws/amazon-ecs-agent/issues/1128
1522
+ assignPublicIp: context.cliOptions.noAssignPublicIp
1523
+ ? 'DISABLED'
1524
+ : 'ENABLED',
1525
+ securityGroups: context.fargateSecurityGroupIds,
1526
+ subnets: context.fargatePublicSubnetIds
1527
+ }
1528
+ };
1529
+ } else {
1530
+ defaultParams.launchType = 'EC2';
1531
+ }
1532
+
1533
+ context.defaultECSParams = defaultParams;
1534
+ return context;
1535
+ }
1536
+
1537
+ async function launchLeadTask(context) {
1538
+ const metadata = {
1539
+ testId: context.testId,
1540
+ startedAt: Date.now(),
1541
+ cluster: context.clusterName,
1542
+ region: context.region,
1543
+ launchType: context.cliOptions.launchType,
1544
+ isFargateSpot: context.isCapacitySpot,
1545
+ count: context.count,
1546
+ sqsQueueUrl: context.sqsQueueUrl,
1547
+ tags: context.tags,
1548
+ secrets: JSON.stringify(
1549
+ Array.isArray(context.extraSecrets)
1550
+ ? context.extraSecrets
1551
+ : [context.extraSecrets]
1552
+ ),
1553
+ platformConfig: JSON.stringify({
1554
+ memory: context.taskDefinition.containerDefinitions[0].memory,
1555
+ cpu: context.taskDefinition.containerDefinitions[0].cpu
1556
+ }),
1557
+ artilleryVersion: JSON.stringify({
1558
+ core: global.artillery.version
1559
+ }),
1560
+ // Properties from the runnable script object:
1561
+ testConfig: {
1562
+ target: context.runnableScript.config.target,
1563
+ phases: context.runnableScript.config.phases,
1564
+ plugins: context.runnableScript.config.plugins,
1565
+ environment: context.runnableScript._environment,
1566
+ scriptPath: context.runnableScript._scriptPath,
1567
+ configPath: context.runnableScript._configPath
1568
+ }
1569
+ };
1570
+
1571
+ artillery.globalEvents.emit('metadata', metadata);
1572
+
1573
+ context.status = TEST_RUN_STATUS.LAUNCHING_WORKERS;
1574
+
1575
+ const ecs = new ECSClient({
1576
+ apiVersion: '2014-11-13',
1577
+ region: context.region
1578
+ });
1579
+
1580
+ const leaderParams = Object.assign(
1581
+ { count: 1 },
1582
+ JSON.parse(JSON.stringify(context.defaultECSParams))
1583
+ );
1584
+ leaderParams.overrides.containerOverrides[0].environment.push({
1585
+ name: 'IS_LEADER',
1586
+ value: 'true'
1587
+ });
1588
+ const runData = await ecs.send(new RunTaskCommand(leaderParams));
1589
+ if (runData.failures.length > 0) {
1590
+ if (runData.failures.length === context.count) {
1591
+ artillery.log('ERROR: Worker start failure');
1592
+ const uniqueReasons = [
1593
+ ...new Set(runData.failures.map((f) => f.reason))
1594
+ ];
1595
+ artillery.log('Reason:', uniqueReasons);
1596
+ throw new Error('Could not start workers');
1597
+ } else {
1598
+ artillery.log('WARNING: Some workers failed to start');
1599
+ artillery.log(chalk.red(JSON.stringify(runData.failures, null, 4)));
1600
+ throw new Error('Not enough capacity - terminating');
1601
+ }
1602
+ }
1603
+
1604
+ context.taskArns = context.taskArns.concat(
1605
+ runData.tasks.map((task) => task.taskArn)
1606
+ );
1607
+ artillery.globalEvents.emit('metadata', {
1608
+ platformMetadata: { taskArns: context.taskArns }
1609
+ });
1610
+
1611
+ return context;
1612
+ }
1613
+
1614
+ // TODO: When launching >20 containers on Fargate, adjust WAIT_TIMEOUT dynamically to
1615
+ // add extra time spent in waiting between runTask calls: WAIT_TIMEOUT + worker_count.
1616
+ async function ecsRunTask(context) {
1617
+ const ecs = new ECSClient({
1618
+ apiVersion: '2014-11-13',
1619
+ region: context.region
1620
+ });
1621
+ let tasksRemaining = context.count - 1;
1622
+ let retries = 0;
1623
+
1624
+ while (
1625
+ tasksRemaining > 0 &&
1626
+ context.status !== TEST_RUN_STATUS.TERMINATING &&
1627
+ context.status !== TEST_RUN_STATUS.EARLY_STOP
1628
+ ) {
1629
+ if (retries >= 10) {
1630
+ artillery.log('Max retries for ECS (10) exceeded');
1631
+ throw new Error('Max retries exceeded');
1632
+ }
1633
+
1634
+ const launchCount = tasksRemaining <= 10 ? tasksRemaining : 10;
1635
+ const params = Object.assign(
1636
+ { count: launchCount },
1637
+ JSON.parse(JSON.stringify(context.defaultECSParams))
1638
+ );
1639
+
1640
+ params.overrides.containerOverrides[0].environment.push({
1641
+ name: 'IS_LEADER',
1642
+ value: 'false'
1643
+ });
1644
+
1645
+ try {
1646
+ const runData = await ecs.send(new RunTaskCommand(params));
1647
+
1648
+ const launchedTasksCount = runData.tasks?.length || 0;
1649
+ tasksRemaining -= launchedTasksCount;
1650
+
1651
+ if (launchedTasksCount > 0) {
1652
+ const newTaskArns = runData.tasks.map((task) => task.taskArn);
1653
+ context.taskArns = context.taskArns.concat(newTaskArns);
1654
+ artillery.globalEvents.emit('metadata', {
1655
+ platformMetadata: { taskArns: newTaskArns }
1656
+ });
1657
+ debug(`Launched ${launchedTasksCount} tasks`);
1658
+ }
1659
+
1660
+ if (runData.failures.length > 0) {
1661
+ artillery.log('Some workers failed to start');
1662
+ const uniqueReasons = [
1663
+ ...new Set(runData.failures.map((f) => f.reason))
1664
+ ];
1665
+ artillery.log(chalk.red(uniqueReasons));
1666
+ artillery.log('Retrying...');
1667
+ await sleep(10 * 1000);
1668
+ throw new Error('Not enough ECS capacity');
1669
+ }
1670
+ } catch (runErr) {
1671
+ if (runErr instanceof ThrottlingException) {
1672
+ artillery.log('ThrottlingException returned from ECS, retrying');
1673
+ await sleep(2000 * retries);
1674
+ debug('runTask throttled, retrying');
1675
+ debug(runErr);
1676
+ } else if (runErr.message.match(/Not enough ECS capacity/gi)) {
1677
+ // Do nothing
1678
+ } else {
1679
+ artillery.log(runErr);
1680
+ }
1681
+
1682
+ retries++;
1683
+ if (retries >= 10) {
1684
+ artillery.log('Max retries for ECS (10) exceeded');
1685
+ throw runErr;
1686
+ }
1687
+ }
1688
+ }
1689
+ return context;
1690
+ }
1691
+
1692
+ async function waitForTasks2(context) {
1693
+ const params = {
1694
+ tasks: context.taskArns,
1695
+ cluster: context.clusterName
1696
+ };
1697
+
1698
+ let failedTasks = [];
1699
+ let stoppedTasks = [];
1700
+ let maybeErr = null;
1701
+
1702
+ const silentWaitTimeout = new Timeout(30 * 1000).start(); // wait this long before updating the user
1703
+ const waitTimeout = new Timeout(60 * 1000).start(); // wait for up to 1 minute
1704
+ while (context.status !== TEST_RUN_STATUS.TERMINATING) {
1705
+ let ecsData;
1706
+ try {
1707
+ ecsData = await awsUtil.ecsDescribeTasks(params, context.region);
1708
+ } catch (err) {
1709
+ // TODO: Inspect err for any conditions in which we may want to abort immediately.
1710
+ // Otherwise, let the timeout run to completion.
1711
+ debug(err);
1712
+ await sleep(5000);
1713
+ continue;
1714
+ }
1715
+
1716
+ // All tasks are RUNNING, proceed:
1717
+ if (_.every(ecsData.tasks, (s) => s.lastStatus === 'RUNNING')) {
1718
+ logProgress('All workers started...');
1719
+ debug('All tasks in RUNNING state');
1720
+ break;
1721
+ }
1722
+
1723
+ // If there are STOPPED tasks, we need to stop:
1724
+ stoppedTasks = ecsData.tasks.filter((t) => t.lastStatus === 'STOPPED');
1725
+ if (stoppedTasks.length > 0) {
1726
+ debug('Some tasks in STOPPED state');
1727
+ debugErr(stoppedTasks);
1728
+ // TODO: Stop RUNNING tasks and clean up (release queue lock, deregister task definition)
1729
+ // TODO: Provide more information here, e.g. task ARNs, or CloudWatch log group ID
1730
+ maybeErr = new Error('Worker init failure, aborting test');
1731
+ break;
1732
+ }
1733
+
1734
+ // If some tasks failed to start altogether, abort:
1735
+ if (ecsData.failures.length > 0) {
1736
+ failedTasks = ecsData.failures;
1737
+ debug('Some tasks failed to start');
1738
+ debugErr(ecsData.failures);
1739
+ maybeErr = new Error('Worker start up failure, aborting test');
1740
+ break;
1741
+ }
1742
+
1743
+ // If there are PENDING, update progress bar
1744
+ debug('Waiting on pending tasks');
1745
+ if (silentWaitTimeout.timedout()) {
1746
+ const statusCounts = _.countBy(ecsData.tasks, 'lastStatus');
1747
+ const statusSummary = _.map(statusCounts, (count, status) => {
1748
+ const displayStatus =
1749
+ status === 'RUNNING' ? 'ready' : status.toLowerCase();
1750
+ let displayStatusChalked = displayStatus;
1751
+ if (displayStatus === 'ready') {
1752
+ displayStatusChalked = chalk.green(displayStatus);
1753
+ } else if (displayStatus === 'pending') {
1754
+ displayStatusChalked = chalk.yellow(displayStatus);
1755
+ }
1756
+
1757
+ return `${displayStatusChalked}: ${count}`;
1758
+ }).join(' / ');
1759
+
1760
+ logProgress(`Waiting for workers to start: ${statusSummary}`);
1761
+ }
1762
+
1763
+ if (waitTimeout.timedout()) {
1764
+ // TODO: Clean up RUNNING tasks etc
1765
+ break;
1766
+ }
1767
+ await sleep(10 * 1000);
1768
+ } // while
1769
+ waitTimeout.stop();
1770
+
1771
+ if (maybeErr) {
1772
+ if (stoppedTasks.length > 0) {
1773
+ artillery.log(stoppedTasks);
1774
+ }
1775
+ if (failedTasks.length > 0) {
1776
+ artillery.log(failedTasks);
1777
+ }
1778
+ throw maybeErr;
1779
+ }
1780
+
1781
+ return context;
1782
+ }
1783
+
1784
+ async function waitForWorkerSync(context) {
1785
+ const MAGIC_PREFIX = 'synced_';
1786
+ const prefix = `test-runs/${context.testId}/${MAGIC_PREFIX}`;
1787
+
1788
+ const intervalSec = 10;
1789
+ const times = WAIT_TIMEOUT / intervalSec;
1790
+ let attempts = 0;
1791
+ let synced = false;
1792
+
1793
+ while (attempts < times) {
1794
+ try {
1795
+ const objects = await util.listAllObjectsWithPrefix(
1796
+ context.s3Bucket,
1797
+ prefix
1798
+ );
1799
+ if (objects.length !== context.count) {
1800
+ attempts++;
1801
+ } else {
1802
+ synced = true;
1803
+ break;
1804
+ }
1805
+ } catch (_err) {
1806
+ attempts++;
1807
+ }
1808
+ await sleep(intervalSec * 1000);
1809
+ }
1810
+
1811
+ if (synced) {
1812
+ return context;
1813
+ } else {
1814
+ throw new Error('Timed out waiting for worker sync');
1815
+ }
1816
+ }
1817
+
1818
+ async function sendGoSignal(context) {
1819
+ const s3 = createS3Client();
1820
+ const params = {
1821
+ Body: context.testId,
1822
+ Bucket: context.s3Bucket,
1823
+ Key: `test-runs/${context.testId}/go.json`
1824
+ };
1825
+ await s3.send(new PutObjectCommand(params));
1826
+ return context;
1827
+ }
1828
+
1829
+ async function listen(context, ee) {
1830
+ return new Promise((resolve, _reject) => {
1831
+ context.intermediateReports = [];
1832
+ context.aggregateReport = null;
1833
+
1834
+ const r = new SqsReporter(context);
1835
+ context.sqsReporter = r;
1836
+ r.on('workersDone', (state) => {
1837
+ ee.emit('workersDone', state);
1838
+ return resolve(context);
1839
+ });
1840
+ r.on('done', (stats) => {
1841
+ if (stats.report) {
1842
+ context.aggregateReport = stats.report();
1843
+ } else {
1844
+ context.aggregateReport = stats;
1845
+ }
1846
+
1847
+ global.artillery.globalEvents.emit('done', stats);
1848
+ ee.emit('done', stats);
1849
+ });
1850
+ r.on('error', (err) => {
1851
+ // Ignore SQS errors
1852
+ // ee.emit('error', err);
1853
+ // return reject(err);
1854
+ debug(err);
1855
+ });
1856
+
1857
+ r.on('workerDone', (body, attrs) => {
1858
+ if (process.env.LOG_WORKER_MESSAGES) {
1859
+ artillery.log(
1860
+ chalk.green(
1861
+ `[${attrs.workerId.StringValue} ${JSON.stringify(body, null, 4)}]`
1862
+ )
1863
+ );
1864
+ }
1865
+ });
1866
+ r.on('workerError', (body, attrs) => {
1867
+ if (process.env.LOG_WORKER_MESSAGES) {
1868
+ artillery.log(
1869
+ chalk.red(
1870
+ `[${attrs.workerId.StringValue} ${JSON.stringify(body, null, 4)}]`
1871
+ )
1872
+ );
1873
+ }
1874
+ if (body.exitCode !== 21) {
1875
+ artillery.log(
1876
+ chalk.yellow(
1877
+ `Worker exited with an error, worker ID = ${attrs.workerId.StringValue}`
1878
+ )
1879
+ );
1880
+ }
1881
+
1882
+ // TODO: Copy log over and print path to log file so that user may inspect it - in a temporary location
1883
+ global.artillery.suggestedExitCode = body.exitCode || 1;
1884
+ });
1885
+
1886
+ r.on('workerMessage', (body, attrs) => {
1887
+ if (process.env.LOG_WORKER_MESSAGES) {
1888
+ artillery.log(
1889
+ chalk.yellow(
1890
+ `[${attrs.workerId.StringValue}] ${body.msg} ${body.type}`
1891
+ )
1892
+ );
1893
+ }
1894
+
1895
+ if (body.type === 'stopped') {
1896
+ if (context.status !== TEST_RUN_STATUS.EARLY_STOP) {
1897
+ artillery.log('Test run has been requested to stop');
1898
+ }
1899
+ context.status = TEST_RUN_STATUS.EARLY_STOP;
1900
+ }
1901
+
1902
+ if (body.type === 'ensure') {
1903
+ try {
1904
+ context.ensureSpec = JSON.parse(util.atob(body.msg));
1905
+ } catch (_parseErr) {
1906
+ console.error('Error processing ensure directive');
1907
+ }
1908
+ }
1909
+
1910
+ if (body.type === 'leader' && body.msg === 'prepack_end') {
1911
+ ee.emit('prepack_end');
1912
+ }
1913
+ });
1914
+
1915
+ r.on('stats', async (stats) => {
1916
+ let report;
1917
+ if (stats.report) {
1918
+ report = stats.report();
1919
+ context.intermediateReports.push(report);
1920
+ } else {
1921
+ context.intermediateReports.push(stats);
1922
+ report = stats;
1923
+ }
1924
+
1925
+ global.artillery.globalEvents.emit('stats', stats);
1926
+ ee.emit('stats', stats);
1927
+ });
1928
+
1929
+ r.on('phaseStarted', (phase) => {
1930
+ global.artillery.globalEvents.emit('phaseStarted', phase);
1931
+ });
1932
+
1933
+ r.on('phaseCompleted', (phase) => {
1934
+ global.artillery.globalEvents.emit('phaseCompleted', phase);
1935
+ });
1936
+
1937
+ r.start();
1938
+ });
1939
+ }
1940
+
1941
+ async function deregisterTaskDefinition(context) {
1942
+ if (!context.taskDefinitionArn) {
1943
+ return;
1944
+ }
1945
+
1946
+ const ecs = new ECSClient({
1947
+ apiVersion: '2014-11-13',
1948
+ region: context.region
1949
+ });
1950
+ try {
1951
+ await ecs.send(
1952
+ new DeregisterTaskDefinitionCommand({
1953
+ taskDefinition: context.taskDefinitionArn
1954
+ })
1955
+ );
1956
+ debug(`Deregistered ${context.taskDefinitionArn}`);
1957
+ } catch (err) {
1958
+ artillery.log(err);
1959
+ debug(err);
1960
+ }
1961
+
1962
+ return context;
1963
+ }
1964
+
1965
+ // TODO: Remove - duplicated in run.js
1966
+ function getLogFilename(output, userDefaultFilenameFormat) {
1967
+ let logfile;
1968
+
1969
+ // is the destination a directory that exists?
1970
+ let isDir = false;
1971
+ if (output) {
1972
+ try {
1973
+ isDir = fs.statSync(output).isDirectory();
1974
+ } catch (_err) {
1975
+ // ENOENT, don't need to do anything
1976
+ }
1977
+ }
1978
+
1979
+ const defaultFormat = '[artillery_report_]YMMDD_HHmmSS[.json]';
1980
+ if (!isDir && output) {
1981
+ // -o is set with a filename (existing or not)
1982
+ logfile = output;
1983
+ } else if (!isDir && !output) {
1984
+ // no -o set
1985
+ } else {
1986
+ // -o is set with a directory
1987
+ logfile = path.join(
1988
+ output,
1989
+ moment().format(userDefaultFilenameFormat || defaultFormat)
1990
+ );
1991
+ }
1992
+
1993
+ return logfile;
1994
+ }