@temporalio/worker
Version:
Temporal.io SDK Worker sub-package
225 lines • 8.63 kB
JavaScript
/**
* Wrapper for starting VM Workflows in Node Worker threads.
* https://nodejs.org/api/worker_threads.html
*
* Worker threads are used here because creating vm contexts is a long running
* operation which blocks the Node.js event loop causing the SDK Worker to
* become unresponsive.
*
* @module
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.VMWorkflowThreadProxy = exports.ThreadedVMWorkflowCreator = exports.WorkerThreadClient = exports.TERMINATED_EXIT_CODE = void 0;
const node_worker_threads_1 = require("node:worker_threads");
const workflow_1 = require("@temporalio/workflow");
const errors_1 = require("../errors");
// https://nodejs.org/api/worker_threads.html#event-exit
exports.TERMINATED_EXIT_CODE = 1;
/**
* Helper to translate errors returned from worker thread to `Error` classes
*/
function errorNameToClass(name) {
switch (name) {
case 'IllegalStateError':
return workflow_1.IllegalStateError;
default:
return Error;
}
}
/**
* Client for communicating with a workflow worker thread.
*
* Uses postMessage to send messages and listens on the `message` event to receive messages.
*/
class WorkerThreadClient {
workerThread;
logger;
requestIdx = 0n;
requestIdToCompletion = new Map();
shutDownRequested = false;
workerExited = false;
activeWorkflowCount = 0;
exitError;
constructor(workerThread, logger) {
this.workerThread = workerThread;
this.logger = logger;
workerThread.on('message', ({ requestId, result }) => {
const completion = this.requestIdToCompletion.get(requestId);
if (completion === undefined) {
throw new workflow_1.IllegalStateError(`Got completion for unknown requestId ${requestId}`);
}
this.requestIdToCompletion.delete(requestId);
if (result.type === 'error') {
const ctor = errorNameToClass(result.name);
const err = new ctor(result.message);
err.stack = result.stack;
completion.reject(err);
return;
}
completion.resolve(result.output);
});
workerThread.on('error', (err) => {
logger.error(`Workflow Worker Thread failed: ${err}`, err);
this.exitError = new errors_1.UnexpectedError(`Workflow Worker Thread exited prematurely: ${err}`, err);
// Node will automatically terminate the Worker Thread, immediately after this event.
});
workerThread.on('exit', (exitCode) => {
logger.trace(`Workflow Worker Thread exited with code ${exitCode}`, { exitError: this.exitError });
this.workerExited = true;
const error = this.exitError ??
new errors_1.UnexpectedError('Workflow Worker Thread exited while there were still pending completions', {
shutDownRequested: this.shutDownRequested,
});
const completions = this.requestIdToCompletion.values();
this.requestIdToCompletion = new Map();
for (const completion of completions) {
completion.reject(error);
}
});
}
/**
* Send input to Worker thread and await for output
*/
async send(input) {
if (this.exitError || this.workerExited) {
throw this.exitError ?? new errors_1.UnexpectedError('Received request after worker thread exited');
}
const requestId = this.requestIdx++;
const request = { requestId, input };
if (request.input.type === 'create-workflow') {
this.activeWorkflowCount++;
}
else if (request.input.type === 'dispose-workflow') {
this.activeWorkflowCount--;
}
this.workerThread.postMessage(request);
return new Promise((resolve, reject) => {
this.requestIdToCompletion.set(requestId, { resolve, reject });
});
}
/**
* Request destruction of the worker thread and await for it to terminate correctly
*/
async destroy() {
if (this.workerExited) {
return;
}
this.shutDownRequested = true;
await this.send({ type: 'destroy' });
const exitCode = await this.workerThread.terminate();
if (exitCode !== exports.TERMINATED_EXIT_CODE) {
throw new errors_1.UnexpectedError(`Failed to terminate Worker thread, exit code: ${exitCode}`);
}
}
getActiveWorkflowCount() {
return this.activeWorkflowCount;
}
}
exports.WorkerThreadClient = WorkerThreadClient;
/**
* A WorkflowCreator that creates vm Workflows inside Worker threads
*/
class ThreadedVMWorkflowCreator {
workerThreadClients;
/**
* Create an instance of ThreadedVMWorkflowCreator asynchronously.
*
* This method creates and initializes the workflow-worker-thread instances.
*/
static async create({ threadPoolSize, workflowBundle, isolateExecutionTimeoutMs, reuseV8Context, registeredActivityNames, logger, }) {
const workerThreadClients = Array(threadPoolSize)
.fill(0)
.map(() => new WorkerThreadClient(new node_worker_threads_1.Worker(require.resolve('./workflow-worker-thread')), logger));
await Promise.all(workerThreadClients.map((client) => client.send({
type: 'init',
workflowBundle,
isolateExecutionTimeoutMs,
reuseV8Context,
registeredActivityNames,
})));
return new this(workerThreadClients);
}
constructor(workerThreadClients) {
this.workerThreadClients = workerThreadClients;
}
/**
* Create a workflow with given options
*/
async createWorkflow(options) {
const workerThreadClient = this.workerThreadClients.reduce((prev, curr) => prev.getActiveWorkflowCount() < curr.getActiveWorkflowCount() ? prev : curr);
return await VMWorkflowThreadProxy.create(workerThreadClient, options);
}
/**
* Destroy and terminate all threads created by this instance
*/
async destroy() {
await Promise.all(this.workerThreadClients.map((client) => client.destroy()));
}
}
exports.ThreadedVMWorkflowCreator = ThreadedVMWorkflowCreator;
/**
* A proxy class used to communicate with a VMWorkflow instance in a worker thread.
*/
class VMWorkflowThreadProxy {
workerThreadClient;
runId;
/**
* Send a create-workflow command to the thread and await for acknowledgement
*/
static async create(workerThreadClient, options) {
// Delete .now because functions can't be serialized / sent to thread.
// Cast to any to avoid type error, since .now is a required field.
// Safe to cast since we immediately set it inside the thread in initRuntime.
delete options.info.unsafe.now;
await workerThreadClient.send({ type: 'create-workflow', options });
return new this(workerThreadClient, options.info.runId);
}
constructor(workerThreadClient, runId) {
this.workerThreadClient = workerThreadClient;
this.runId = runId;
}
/**
* Proxy request to the VMWorkflow instance
*/
async getAndResetSinkCalls() {
const output = await this.workerThreadClient.send({
type: 'extract-sink-calls',
runId: this.runId,
});
if (output?.type !== 'sink-calls') {
throw new TypeError(`Got invalid response output from Workflow Worker thread ${output}`);
}
output.calls.forEach((call) => {
call.workflowInfo.unsafe.now = Date.now;
});
return output.calls;
}
/**
* Proxy request to the VMWorkflow instance
*/
async activate(activation) {
const output = await this.workerThreadClient.send({
type: 'activate-workflow',
activation,
runId: this.runId,
});
if (output?.type !== 'activation-completion') {
throw new TypeError(`Got invalid response output from Workflow Worker thread ${output}`);
}
return output.completion;
}
/**
* Proxy request to the VMWorkflow instance
*/
async dispose() {
try {
await this.workerThreadClient.send({ type: 'dispose-workflow', runId: this.runId });
}
catch (_e) {
// Ignore errors when disposing
}
}
}
exports.VMWorkflowThreadProxy = VMWorkflowThreadProxy;
//# sourceMappingURL=threaded-vm.js.map
;