artillery
Version:
Cloud-scale load testing. https://www.artillery.io
182 lines (156 loc) • 5.54 kB
text/typescript
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import path from 'node:path';
import { Worker } from 'node:worker_threads';
import { EventEmitter } from 'eventemitter3';
import awaitOnEE from '../../util/await-on-ee.ts';
import STATES from '../worker-states.ts';
import type { WorkerState } from '../worker-states.ts';
import type {
PrepareWorkerOptions,
WorkerCommand,
WorkerEnvelope
} from './protocol.ts';
import type { StashDetails } from '../../stash.ts';
// Emitted output runs from dist/ where the sibling module is worker.js;
// when this file is loaded directly from source (tests importing .ts),
// the sibling is worker.ts (worker threads strip types natively).
const workerModulePath = path.join(
import.meta.dirname,
import.meta.filename.endsWith('.ts') ? 'worker.ts' : 'worker.js'
);
const returnWorkerEnv = (needsSourcemap: unknown) => {
const env = { ...process.env };
if (needsSourcemap) {
env.NODE_OPTIONS = process.env.NODE_OPTIONS
? `${process.env.NODE_OPTIONS} --enable-source-maps`
: '--enable-source-maps';
}
return env;
};
class ArtilleryWorker {
declare opts: unknown;
declare events: EventEmitter;
declare workerEvents: EventEmitter;
declare worker: Worker;
declare workerId: number;
declare state: WorkerState;
constructor(opts?: unknown) {
this.opts = opts;
this.events = new EventEmitter(); // events for consumers of this object
this.workerEvents = new EventEmitter(); // turn events delivered via 'message' events into their own messages
}
async init(_opts?: unknown) {
this.state = STATES.initializing;
const workerEnv = returnWorkerEnv(global.artillery.hasTypescriptProcessor);
this.worker = new Worker(workerModulePath, {
env: workerEnv
});
this.workerId = this.worker.threadId;
this.worker.on('error', this.onError.bind(this));
// TODO:
this.worker.on('exit', (exitCode: number) => {
this.events.emit('exit', exitCode);
});
this.worker.on('messageerror', (_err: Error) => {});
// TODO: Expose performance metrics via getHeapSnapshot() and performance object.
await awaitOnEE(this.worker, 'online', 10);
// Relay messages onto the real event emitter:
this.worker.on('message', (message: WorkerEnvelope) => {
switch (message.event) {
case 'log':
this.events.emit('log', message);
this.workerEvents.emit('log', message);
break;
case 'workerError':
this.events.emit('workerError', message);
this.workerEvents.emit('workerError', message);
break;
case 'phaseStarted':
this.events.emit('phaseStarted', message);
this.workerEvents.emit('phaseStarted', message);
break;
case 'phaseCompleted':
this.events.emit('phaseCompleted', message);
this.workerEvents.emit('phaseCompleted', message);
break;
case 'stats':
this.events.emit('stats', message);
this.workerEvents.emit('stats', message);
break;
case 'done':
this.events.emit('done', message);
this.workerEvents.emit('done', message);
break;
case 'running':
this.events.emit('running', message);
this.workerEvents.emit('running', message);
break;
case 'readyWaiting':
this.events.emit('readyWaiting', message);
this.workerEvents.emit('readyWaiting', message);
break;
case 'setSuggestedExitCode':
this.events.emit('setSuggestedExitCode', message);
break;
default:
global.artillery.log(
`Unknown message from worker ${message}`,
'error'
);
}
});
this.state = STATES.online;
}
async prepare(opts: {
script: Record<string, any>;
payload: unknown;
options: Record<string, any>;
stashDetails?: StashDetails | null;
}) {
this.state = STATES.preparing;
const { script, payload, options, stashDetails } = opts;
let scriptForWorker: Record<string, any> = script;
if (script.__transpiledTypeScriptPath && script.__originalScriptPath) {
scriptForWorker = {
__transpiledTypeScriptPath: script.__transpiledTypeScriptPath,
__originalScriptPath: script.__originalScriptPath,
__phases: script.config?.phases
};
}
const prepareOpts: PrepareWorkerOptions = {
script: scriptForWorker,
payload,
options,
testRunId: global.artillery.testRunId,
stashDetails
};
const command: WorkerCommand = {
command: 'prepare',
opts: prepareOpts
};
this.worker.postMessage(command);
await awaitOnEE(this.workerEvents, 'readyWaiting', 50);
this.state = STATES.readyWaiting;
}
async run(opts: string) {
const command: WorkerCommand = {
command: 'run',
opts: JSON.parse(opts)
};
this.worker.postMessage(command);
await awaitOnEE(this.workerEvents, 'running', 50);
this.state = STATES.running;
}
async stop() {
const command: WorkerCommand = { command: 'stop' };
this.worker.postMessage(command);
}
onError(err: Error) {
// TODO: set state, clean up
this.events.emit('error', err);
console.log('worker error, id:', this.workerId, err);
}
}
export { ArtilleryWorker, STATES };