UNPKG

@temporalio/worker

Version:
225 lines 8.63 kB
"use strict"; /** * 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