UNPKG

@temporalio/workflow

Version:
1,095 lines 54.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Activator = void 0; const common_1 = require("@temporalio/common"); const payload_search_attributes_1 = require("@temporalio/common/lib/converter/payload-search-attributes"); const internal_workflow_1 = require("@temporalio/common/lib/internal-workflow"); const reserved_1 = require("@temporalio/common/lib/reserved"); const alea_1 = require("./alea"); const cancellation_scope_1 = require("./cancellation-scope"); const interceptor_composition_1 = require("./interceptor-composition"); const update_scope_1 = require("./update-scope"); const random_stream_seed_1 = require("./random-stream-seed"); const errors_1 = require("./errors"); const interfaces_1 = require("./interfaces"); const stack_helpers_1 = require("./stack-helpers"); const pkg_1 = __importDefault(require("./pkg")); const flags_1 = require("./flags"); const logs_1 = require("./logs"); const StartChildWorkflowExecutionFailedCause = { WORKFLOW_ALREADY_EXISTS: 'WORKFLOW_ALREADY_EXISTS', }; const [_encodeStartChildWorkflowExecutionFailedCause, decodeStartChildWorkflowExecutionFailedCause] = (0, internal_workflow_1.makeProtoEnumConverters)({ [StartChildWorkflowExecutionFailedCause.WORKFLOW_ALREADY_EXISTS]: 1, UNSPECIFIED: 0, }, 'START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_'); /** * Keeps all of the Workflow runtime state like pending completions for activities and timers. * * Implements handlers for all workflow activation jobs. * * Note that most methods in this class are meant to be called only from within the VM. * * However, a few methods may be called directly from outside the VM (essentially from `vm-shared.ts`). * These methods are specifically marked with a comment and require careful consideration, as the * execution context may not properly reflect that of the target workflow execution (e.g.: with Reusable * VMs, the `global` may not have been swapped to those of that workflow execution; the active microtask * queue may be that of the thread/process, rather than the queue of that VM context; etc). Consequently, * methods that are meant to be called from outside of the VM must not do any of the following: * * - Access any global variable; * - Create Promise objects, use async/await, or otherwise schedule microtasks; * - Call user-defined functions, including any form of interceptor. */ class Activator { /** * Cache for modules - referenced in reusable-vm.ts */ moduleCache = new Map(); /** * Map of task sequence to a Completion */ completions = { timer: new Map(), activity: new Map(), nexusOperationStart: new Map(), nexusOperationComplete: new Map(), childWorkflowStart: new Map(), childWorkflowComplete: new Map(), signalWorkflow: new Map(), cancelWorkflow: new Map(), }; /** * Holds buffered Update calls until a handler is registered */ bufferedUpdates = Array(); /** * Holds buffered signal calls until a handler is registered */ bufferedSignals = Array(); /** * Mapping of update name to handler and validator */ updateHandlers = new Map(); /** * Mapping of signal name to handler */ signalHandlers = new Map(); /** * Mapping of in-progress updates to handler execution information. */ inProgressUpdates = new Map(); /** * Mapping of in-progress signals to handler execution information. */ inProgressSignals = new Map(); /** * A sequence number providing unique identifiers for signal handler executions. */ signalHandlerExecutionSeq = 0; /** * A signal handler that catches calls for non-registered signal names. */ defaultSignalHandler; /** * A update handler that catches calls for non-registered update names. */ defaultUpdateHandler; /** * A query handler that catches calls for non-registered query names. */ defaultQueryHandler; /** * Source map file for looking up the source files in response to __enhanced_stack_trace */ sourceMap; /** * Whether or not to send the sources in enhanced stack trace query responses */ showStackTraceSources; promiseStackStore = { promiseToStack: new Map(), childToParent: new Map(), }; /** * The error that caused the current Workflow Task to fail. Sets if a non-`TemporalFailure` * error bubbles up out of the Workflow function, or out of a Signal or Update handler. We * capture errors this way because those functions are not technically awaited when started, * but left to run asynchronously. There is therefore no real "parent" function that can * directly handle those errors, and not capturing it would result in an Unhandled Promise * Rejection. So instead, we buffer the error here, to then be processed in the context * of our own synchronous Activation handling event loop. * * Our code does a best effort to stop processing the current activation as soon as possible * after this field is set: * - If an error is thrown while executing code synchronously (e.g. anything before the * first `await` statement in a Workflow function or a signal/update handler), the error * will be _immediately_ rethrown, which will prevent execution of further jobs in the * current activation. We know we're currently running code synchronously thanks to the * `rethrowSynchronously` flag below. * - It an error is thrown while executing microtasks, then the error will be rethrown on * the next call to `tryUnblockConditions()`. * * Unfortunately, there's no way for us to prevent further execution of microtasks that have * already been scheduled, nor those that will be recursively scheduled from those microtasks. * Should more errors get thrown while settling microtasks, those will be ignored (i.e. only * the first captured error is preserved). */ workflowTaskError; /** * Error type _names_ (from {@link WorkerOptions.workflowFailureErrorTypes}) that * should cause Workflow Execution failure rather than WFT failure. * * Set at workflow creation time from the worker options. */ failureExceptionTypeNames = []; /** * Error _types_ (from {@link WorkflowDefinitionOptions.failureExceptionTypes}) * that should cause Workflow Execution failure rather than WFT failure. * * Set in `worker-interface.ts` after the workflow definition options are read. */ workflowDefinitionFailureExceptionTypes = undefined; /** * Set to true when running synchronous code (e.g. while processing activation jobs and when calling * `tryUnblockConditions()`). While this flag is set, it is safe to let errors bubble up. */ rethrowSynchronously = false; rootScope = new cancellation_scope_1.RootCancellationScope(); /** * Mapping of query name to handler */ queryHandlers = new Map([ [ reserved_1.STACK_TRACE_QUERY_NAME, { handler: () => { return new common_1.RawValue(this.getStackTraces() .map((s) => s.formatted) .join('\n\n')); }, description: 'Returns a sensible stack trace.', }, ], [ reserved_1.ENHANCED_STACK_TRACE_QUERY_NAME, { handler: () => { const { sourceMap } = this; const sdk = { name: 'typescript', version: pkg_1.default.version }; const stacks = this.getStackTraces().map(({ structured: locations }) => ({ locations })); const sources = {}; if (this.showStackTraceSources) { for (const { locations } of stacks) { for (const { file_path } of locations) { if (!file_path) continue; const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(file_path)]; if (!content) continue; sources[file_path] = [ { line_offset: 0, content, }, ]; } } } return new common_1.RawValue({ sdk, stacks, sources }); }, description: 'Returns a stack trace annotated with source information.', }, ], [ '__temporal_workflow_metadata', { handler: () => { const workflowType = this.info.workflowType; const queryDefinitions = Array.from(this.queryHandlers.entries()).map(([name, value]) => ({ name, description: value.description, })); const signalDefinitions = Array.from(this.signalHandlers.entries()).map(([name, value]) => ({ name, description: value.description, })); const updateDefinitions = Array.from(this.updateHandlers.entries()).map(([name, value]) => ({ name, description: value.description, })); return new common_1.RawValue({ definition: { type: workflowType, queryDefinitions, signalDefinitions, updateDefinitions, }, currentDetails: this.currentDetails, }); }, description: 'Returns metadata associated with this workflow.', }, ], ]); /** * Loaded in {@link initRuntime} */ interceptors = { inbound: [], outbound: [], internals: [], }; /** * Buffer that stores all generated commands, reset after each activation */ commands = []; /** * Stores all {@link condition}s that haven't been unblocked yet */ blockedConditions = new Map(); /** * Is this Workflow completed? * * A Workflow will be considered completed if it generates a command that the * system considers as a final Workflow command (e.g. * completeWorkflowExecution or failWorkflowExecution). */ completed = false; /** * Was this Workflow cancelled? */ cancelled = false; /** * The next (incremental) sequence to assign when generating completable commands */ nextSeqs = { timer: 1, activity: 1, childWorkflow: 1, signalWorkflow: 1, cancelWorkflow: 1, condition: 1, nexusOperation: 1, // Used internally to keep track of active stack traces stack: 1, }; /** * This is set every time the workflow executes an activation * May be accessed and modified from outside the VM. */ now; /** * Reference to the current Workflow, initialized when a Workflow is started */ workflow; /** * Information about the current Workflow * May be accessed from outside the VM. */ info; /** * The main deterministic RNG for this workflow execution. * * Scoped overrides used by `WorkflowRandomStream.with(...)` are layered on top of this RNG. */ random; /** * The current seed material for this workflow execution's deterministic RNGs. */ randomnessSeed; /** * Additional deterministic RNG streams keyed by stable stream name. */ namedRandomStreams = new Map(); currentRandomStorage; payloadConverter = common_1.defaultPayloadConverter; failureConverter = common_1.defaultFailureConverter; /** * Patches we know the status of for this workflow, as in {@link patched} */ knownPresentPatches = new Set(); /** * Patches we sent to core {@link patched} */ sentPatches = new Set(); knownFlags = new Set(); sdkVersion; /** * Buffered sink calls per activation */ sinkCalls = Array(); /** * A nanosecond resolution time function, externally injected. This is used to * precisely sort logs entries emitted from the Workflow Context vs those emitted * from other sources (e.g. main thread, Core, etc). */ getTimeOfDay; registeredActivityNames; currentDetails = ''; versioningBehavior; workflowDefinitionOptionsGetter; workflowSandboxDestructors = []; stackTracesEnabled; constructor({ info, now, showStackTraceSources, sourceMap, getTimeOfDay, randomnessSeed, registeredActivityNames, stackTracesEnabled, failureExceptionTypeNames, }) { this.getTimeOfDay = getTimeOfDay; this.info = info; this.now = now; this.showStackTraceSources = showStackTraceSources; this.sourceMap = sourceMap; this.randomnessSeed = [...randomnessSeed]; this.random = (0, alea_1.alea)(this.randomnessSeed); this.registeredActivityNames = registeredActivityNames; this.stackTracesEnabled = stackTracesEnabled; this.failureExceptionTypeNames = failureExceptionTypeNames ?? []; } setRandomnessSeed(randomnessSeed) { this.randomnessSeed = [...randomnessSeed]; this.random = (0, alea_1.alea)(this.randomnessSeed); this.namedRandomStreams.clear(); } getNamedRandom(name) { const cached = this.namedRandomStreams.get(name); if (cached !== undefined) { return cached; } const random = (0, alea_1.alea)((0, random_stream_seed_1.deriveAleaSeed)(this.randomnessSeed, name)); this.namedRandomStreams.set(name, random); return random; } withRandomSource(randomSource, fn) { return (this.currentRandomStorage ??= new update_scope_1.AsyncLocalStorage()).run(randomSource, fn); } withCurrentRandom(randomSource, fn) { return this.withRandomSource(randomSource, fn); } bindCurrentRandom(fn) { const randomSource = this.currentRandomStorage?.getStore(); return ((...args) => this.withRandomSource(randomSource, () => fn(...args))); } currentRandom() { return this.currentRandomStorage?.getStore()?.random() ?? this.random(); } /** * May be invoked from outside the VM. */ mutateWorkflowInfo(fn) { this.info = fn(this.info); } getStackTraces() { if (!this.stackTracesEnabled) { throw new common_1.IllegalStateError('Workflow stack traces are not enabled on this worker'); } const { childToParent, promiseToStack } = this.promiseStackStore; const internalNodes = [...childToParent.values()].reduce((acc, curr) => { for (const p of curr) { acc.add(p); } return acc; }, new Set()); const stacks = new Map(); for (const child of childToParent.keys()) { if (!internalNodes.has(child)) { const stack = promiseToStack.get(child); if (!stack || !stack.formatted) continue; stacks.set(stack.formatted, stack); } } // Not 100% sure where this comes from, just filter it out stacks.delete(' at Promise.then (<anonymous>)'); stacks.delete(' at Promise.then (<anonymous>)\n'); return [...stacks].map(([_, stack]) => stack); } /** * May be invoked from outside the VM. */ getAndResetSinkCalls() { const { sinkCalls } = this; this.sinkCalls = []; return sinkCalls; } /** * Buffer a Workflow command to be collected at the end of the current activation. * * Prevents commands from being added after Workflow completion. */ pushCommand(cmd, complete = false) { this.commands.push(cmd); if (complete) { this.completed = true; } } concludeActivation() { return { commands: this.commands.splice(0), usedInternalFlags: [...this.knownFlags], versioningBehavior: this.versioningBehavior, }; } async startWorkflowNextHandler({ args }) { const { workflow } = this; if (workflow == null) { throw new common_1.IllegalStateError('Workflow uninitialized'); } return await workflow(...args); } startWorkflow(activation) { const execute = (0, interceptor_composition_1.composeInterceptors)(this.interceptors.inbound, 'execute', this.startWorkflowNextHandler.bind(this)); const context = this.workflowSerializationContext(); (0, stack_helpers_1.untrackPromise)((0, logs_1.executeWithLifecycleLogging)(() => execute({ headers: activation.headers ?? {}, args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.arguments, context), })).then(this.completeWorkflow.bind(this), this.handleWorkflowFailure.bind(this))); } initializeWorkflow(activation) { const { continuedFailure, lastCompletionResult, memo, searchAttributes } = activation; const context = this.workflowSerializationContext(); // Most things related to initialization have already been handled in the constructor this.mutateWorkflowInfo((info) => ({ ...info, searchAttributes: (0, payload_search_attributes_1.decodeSearchAttributes)(searchAttributes?.indexedFields), typedSearchAttributes: (0, payload_search_attributes_1.decodeTypedSearchAttributes)(searchAttributes?.indexedFields), memo: (0, common_1.mapFromPayloads)(this.payloadConverter, memo?.fields, context), lastResult: (0, common_1.fromPayloadsAtIndex)(this.payloadConverter, 0, lastCompletionResult?.payloads, context), lastFailure: continuedFailure != null ? this.failureConverter.failureToError(continuedFailure, this.payloadConverter, context) : undefined, })); const workflowDefinitionOpts = this.workflowDefinitionOptionsGetter?.(); if (workflowDefinitionOpts) { this.versioningBehavior = workflowDefinitionOpts.versioningBehavior; this.workflowDefinitionFailureExceptionTypes = workflowDefinitionOpts.failureExceptionTypes; } } cancelWorkflow(_activation) { this.cancelled = true; this.rootScope.cancel(); } fireTimer(activation) { // Timers are a special case where their completion might not be in Workflow state, // this is due to immediate timer cancellation that doesn't go wait for Core. const completion = this.maybeConsumeCompletion('timer', getSeq(activation)); completion?.resolve(undefined); } resolveActivity(activation) { if (!activation.result) { throw new TypeError('Got ResolveActivity activation with no result'); } const { resolve, reject, context } = this.consumeCompletion('activity', getSeq(activation)); if (activation.result.completed) { const completed = activation.result.completed; const result = completed.result ? this.payloadConverter.fromPayload(completed.result, context) : undefined; resolve(result); } else if (activation.result.failed) { const { failure } = activation.result.failed; if (failure == null) { throw new TypeError('Got failed result with no failure attribute'); } reject(this.failureConverter.failureToError(failure, this.payloadConverter, context)); } else if (activation.result.cancelled) { const { failure } = activation.result.cancelled; if (failure == null) { throw new TypeError('Got cancelled result with no failure attribute'); } reject(this.failureConverter.failureToError(failure, this.payloadConverter, context)); } else if (activation.result.backoff) { reject(new errors_1.LocalActivityDoBackoff(activation.result.backoff)); } } resolveChildWorkflowExecutionStart(activation) { const { resolve, reject, context } = this.consumeCompletion('childWorkflowStart', getSeq(activation)); if (activation.succeeded) { if (!activation.succeeded.runId) { throw new TypeError('Got ResolveChildWorkflowExecutionStart with no runId'); } resolve(activation.succeeded.runId); } else if (activation.failed) { if (decodeStartChildWorkflowExecutionFailedCause(activation.failed.cause) !== 'WORKFLOW_ALREADY_EXISTS') { throw new common_1.IllegalStateError('Got unknown StartChildWorkflowExecutionFailedCause'); } if (!(activation.seq && activation.failed.workflowId && activation.failed.workflowType)) { throw new TypeError('Missing attributes in activation job'); } reject(new common_1.WorkflowExecutionAlreadyStartedError('Workflow execution already started', activation.failed.workflowId, activation.failed.workflowType)); } else if (activation.cancelled) { if (!activation.cancelled.failure) { throw new TypeError('Got no failure in cancelled variant'); } reject(this.failureConverter.failureToError(activation.cancelled.failure, this.payloadConverter, context)); } else { throw new TypeError('Got ResolveChildWorkflowExecutionStart with no status'); } } resolveChildWorkflowExecution(activation) { if (!activation.result) { throw new TypeError('Got ResolveChildWorkflowExecution activation with no result'); } const { resolve, reject, context } = this.consumeCompletion('childWorkflowComplete', getSeq(activation)); if (activation.result.completed) { const completed = activation.result.completed; const result = completed.result ? this.payloadConverter.fromPayload(completed.result, context) : undefined; resolve(result); } else if (activation.result.failed) { const { failure } = activation.result.failed; if (failure == null) { throw new TypeError('Got failed result with no failure attribute'); } reject(this.failureConverter.failureToError(failure, this.payloadConverter, context)); } else if (activation.result.cancelled) { const { failure } = activation.result.cancelled; if (failure == null) { throw new TypeError('Got cancelled result with no failure attribute'); } reject(this.failureConverter.failureToError(failure, this.payloadConverter, context)); } } resolveNexusOperationStart(activation) { const seq = getSeq(activation); const { resolve, reject } = this.consumeCompletion('nexusOperationStart', seq); if (!activation.failed) { const completePromise = new Promise((resolve, reject) => { this.completions.nexusOperationComplete.set(seq, { resolve, reject, }); }); (0, stack_helpers_1.untrackPromise)(completePromise); (0, stack_helpers_1.untrackPromise)(completePromise.catch(() => undefined)); resolve({ token: activation.operationToken, result: completePromise }); } else { reject(this.failureToError(activation.failed)); } } resolveNexusOperation(activation) { const seq = getSeq(activation); const context = this.workflowSerializationContext(); if (activation.result?.completed) { const result = this.payloadConverter.fromPayload(activation.result.completed, context); // It is possible for ResolveNexusOperation to be received without a prior ResolveNexusOperationStart, // e.g. because the handler completed the Operation synchronously. const startCompletion = this.maybeConsumeCompletion('nexusOperationStart', seq); if (startCompletion) { startCompletion.resolve({ result: Promise.resolve(result) }); } else { this.consumeCompletion('nexusOperationComplete', seq).resolve(result); } } else { let err; if (activation.result?.failed) { err = this.failureToError(activation.result.failed); } else if (activation.result?.cancelled) { err = this.failureToError(activation.result.cancelled); } else if (activation.result?.timedOut) { err = this.failureToError(activation.result.timedOut); } const completion = this.maybeConsumeCompletion('nexusOperationStart', seq) ?? this.consumeCompletion('nexusOperationComplete', seq); completion.reject(err); } } // Intentionally non-async function so this handler doesn't show up in the stack trace queryWorkflowNextHandler({ queryName, args }) { let fn = this.queryHandlers.get(queryName)?.handler; if (fn === undefined && this.defaultQueryHandler !== undefined) { fn = this.defaultQueryHandler.bind(undefined, queryName); } // No handler or default registered, fail. if (fn === undefined) { const knownQueryTypes = [...this.queryHandlers.keys()].join(' '); // Fail the query return Promise.reject(new ReferenceError(`Workflow did not register a handler for ${queryName}. Registered queries: [${knownQueryTypes}]`)); } // Execute handler. try { const ret = fn(...args); if (ret instanceof Promise) { return Promise.reject(new errors_1.DeterminismViolationError('Query handlers should not return a Promise')); } return Promise.resolve(ret); } catch (err) { return Promise.reject(err); } } queryWorkflow(activation) { const { queryType, queryId, headers } = activation; if (!(queryType && queryId)) { throw new TypeError('Missing query activation attributes'); } // Reject __temporal_-prefixed queries that would otherwise be routed to the // user's default handler. A specific registered handler (e.g. from a // contrib package) is allowed through. if (queryType.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) && !this.queryHandlers.has(queryType) && this.defaultQueryHandler !== undefined) { throw new TypeError(`Cannot use query name: '${queryType}', with reserved prefix: '${reserved_1.TEMPORAL_RESERVED_PREFIX}'`); } // Skip interceptors if it's an internal query. const isInternalQuery = queryType.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) || queryType === reserved_1.STACK_TRACE_QUERY_NAME || queryType === reserved_1.ENHANCED_STACK_TRACE_QUERY_NAME; const interceptors = isInternalQuery ? [] : this.interceptors.inbound; const execute = (0, interceptor_composition_1.composeInterceptors)(interceptors, 'handleQuery', this.queryWorkflowNextHandler.bind(this)); const context = this.workflowSerializationContext(); execute({ queryName: queryType, args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.arguments, context), queryId, headers: headers ?? {}, }).then((result) => this.completeQuery(queryId, result), (reason) => this.failQuery(queryId, reason)); } doUpdate(activation) { const { id: updateId, protocolInstanceId, name, headers, runValidator } = activation; if (!updateId) { throw new TypeError('Missing activation update id'); } if (!name) { throw new TypeError('Missing activation update name'); } if (!protocolInstanceId) { throw new TypeError('Missing activation update protocolInstanceId'); } // Reject __temporal_-prefixed updates that would otherwise be routed to the // user's default handler. A specific registered handler (e.g. from a // contrib package) is allowed through, and unregistered names without a // default handler fall through to the buffer-then-reject path below. if (name.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) && !this.updateHandlers.has(name) && this.defaultUpdateHandler !== undefined) { throw new TypeError(`Cannot use update name: '${name}', with reserved prefix: '${reserved_1.TEMPORAL_RESERVED_PREFIX}'`); } // Skip interceptors if it's an internal update. const isInternalUpdate = name.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) || name === reserved_1.STACK_TRACE_QUERY_NAME || name === reserved_1.ENHANCED_STACK_TRACE_QUERY_NAME; const interceptors = isInternalUpdate ? [] : this.interceptors.inbound; const entry = this.updateHandlers.get(name) ?? (this.defaultUpdateHandler ? { handler: this.defaultUpdateHandler.bind(undefined, name), validator: undefined, // Default to a warning policy. unfinishedPolicy: common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON, } : null); // If we don't have an entry from either source, buffer and return if (entry == null) { this.bufferedUpdates.push(activation); return; } const makeInput = () => { const context = this.workflowSerializationContext(); return { updateId, args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.input, context), name, headers: headers ?? {}, }; }; // The implementation below is responsible for upholding, and constrained // by, the following contract: // // 1. If no validator is present then validation interceptors will not be run. // // 2. During validation, any error must fail the Update; during the Update // itself, Temporal errors fail the Update whereas other errors fail the // activation. // // 3. The handler must not see any mutations of the arguments made by the // validator. // // 4. Any error when decoding/deserializing input must be caught and result // in rejection of the Update before it is accepted, even if there is no // validator. // // 5. The initial synchronous portion of the (async) Update handler should // be executed after the (sync) validator completes such that there is // minimal opportunity for a different concurrent task to be scheduled // between them. // // 6. The stack trace view provided in the Temporal UI must not be polluted // by promises that do not derive from user code. This implies that // async/await syntax may not be used. // // Note that there is a deliberately unhandled promise rejection below. // These are caught elsewhere and fail the corresponding activation. const doUpdateImpl = async () => { let input; try { if (runValidator && entry.validator) { // Temporarily mark as not replaying history events during validator execution // so that logging is permitted. Validators are live read-only operations. const wasReplayingHistoryEvents = this.info.unsafe.isReplayingHistoryEvents; this.mutateWorkflowInfo((info) => ({ ...info, unsafe: { ...info.unsafe, isReplayingHistoryEvents: false }, })); try { const validate = (0, interceptor_composition_1.composeInterceptors)(interceptors, 'validateUpdate', this.validateUpdateNextHandler.bind(this, entry.validator)); validate(makeInput()); } finally { this.mutateWorkflowInfo((info) => ({ ...info, unsafe: { ...info.unsafe, isReplayingHistoryEvents: wasReplayingHistoryEvents }, })); } } input = makeInput(); } catch (error) { this.rejectUpdate(protocolInstanceId, error); return; } this.acceptUpdate(protocolInstanceId); const execute = (0, interceptor_composition_1.composeInterceptors)(interceptors, 'handleUpdate', this.updateNextHandler.bind(this, entry.handler)); const { unfinishedPolicy } = entry; this.inProgressUpdates.set(updateId, { name, unfinishedPolicy, id: updateId }); const res = execute(input) .then((result) => this.completeUpdate(protocolInstanceId, result)) .catch((error) => { if (error instanceof common_1.TemporalFailure) { this.rejectUpdate(protocolInstanceId, error); } else { this.handleWorkflowFailure(error); } }) .finally(() => this.inProgressUpdates.delete(updateId)); (0, stack_helpers_1.untrackPromise)(res); return res; }; (0, stack_helpers_1.untrackPromise)(update_scope_1.UpdateScope.updateWithInfo(updateId, name, doUpdateImpl)); } async updateNextHandler(handler, { args }) { return await handler(...args); } validateUpdateNextHandler(validator, { args }) { if (validator) { validator(...args); } } dispatchBufferedUpdates() { const bufferedUpdates = this.bufferedUpdates; while (bufferedUpdates.length) { // We have a default update handler, so all updates are dispatchable. if (this.defaultUpdateHandler) { const update = bufferedUpdates.shift(); // Logically, this must be defined as we're in the loop. // But Typescript doesn't know that so we use a non-null assertion (!). this.doUpdate(update); } else { const foundIndex = bufferedUpdates.findIndex((update) => this.updateHandlers.has(update.name)); if (foundIndex === -1) { // No buffered Updates have a handler yet. break; } const [update] = bufferedUpdates.splice(foundIndex, 1); this.doUpdate(update); } } } rejectBufferedUpdates() { while (this.bufferedUpdates.length) { const update = this.bufferedUpdates.shift(); if (update) { this.rejectUpdate(update.protocolInstanceId, common_1.ApplicationFailure.nonRetryable(`No registered handler for update: ${update.name}`)); } } } async signalWorkflowNextHandler({ signalName, args }) { const fn = this.signalHandlers.get(signalName)?.handler; if (fn) { return await fn(...args); } else if (this.defaultSignalHandler) { return await this.defaultSignalHandler(signalName, ...args); } else { throw new common_1.IllegalStateError(`No registered signal handler for signal: ${signalName}`); } } signalWorkflow(activation) { const { signalName, headers } = activation; if (!signalName) { throw new TypeError('Missing activation signalName'); } // Reject __temporal_-prefixed signals that would otherwise be routed to the // user's default handler. A specific registered handler (e.g. from a // contrib package) is allowed through, and unregistered names without a // default handler fall through to the buffer-then-reject path below. if (signalName.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) && !this.signalHandlers.has(signalName) && this.defaultSignalHandler !== undefined) { throw new TypeError(`Cannot use signal name: '${signalName}', with reserved prefix: '${reserved_1.TEMPORAL_RESERVED_PREFIX}'`); } // Skip interceptors if it's an internal signal. const isInternalSignal = signalName.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) || signalName === reserved_1.STACK_TRACE_QUERY_NAME || signalName === reserved_1.ENHANCED_STACK_TRACE_QUERY_NAME; const interceptors = isInternalSignal ? [] : this.interceptors.inbound; if (!this.signalHandlers.has(signalName) && !this.defaultSignalHandler) { this.bufferedSignals.push(activation); return; } // If we fall through to the default signal handler then the unfinished // policy is WARN_AND_ABANDON; users currently have no way to silence any // ensuing warnings. const unfinishedPolicy = this.signalHandlers.get(signalName)?.unfinishedPolicy ?? common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON; const signalExecutionNum = this.signalHandlerExecutionSeq++; this.inProgressSignals.set(signalExecutionNum, { name: signalName, unfinishedPolicy }); const execute = (0, interceptor_composition_1.composeInterceptors)(interceptors, 'handleSignal', this.signalWorkflowNextHandler.bind(this)); const context = this.workflowSerializationContext(); execute({ args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.input, context), signalName, headers: headers ?? {}, }) .catch(this.handleWorkflowFailure.bind(this)) .finally(() => this.inProgressSignals.delete(signalExecutionNum)); } dispatchBufferedSignals() { const bufferedSignals = this.bufferedSignals; while (bufferedSignals.length) { if (this.defaultSignalHandler) { // We have a default signal handler, so all signals are dispatchable this.signalWorkflow(bufferedSignals.shift()); } else { const foundIndex = bufferedSignals.findIndex((signal) => this.signalHandlers.has(signal.signalName)); if (foundIndex === -1) break; const [signal] = bufferedSignals.splice(foundIndex, 1); this.signalWorkflow(signal); } } } resolveSignalExternalWorkflow(activation) { const { resolve, reject, context } = this.consumeCompletion('signalWorkflow', getSeq(activation)); if (activation.failure) { reject(this.failureConverter.failureToError(activation.failure, this.payloadConverter, context)); } else { resolve(undefined); } } resolveRequestCancelExternalWorkflow(activation) { const { resolve, reject, context } = this.consumeCompletion('cancelWorkflow', getSeq(activation)); if (activation.failure) { reject(this.failureConverter.failureToError(activation.failure, this.payloadConverter, context)); } else { resolve(undefined); } } warnIfUnfinishedHandlers() { if (this.workflowTaskError) return; const getWarnable = (handlerExecutions) => { return Array.from(handlerExecutions).filter((ex) => ex.unfinishedPolicy === common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON); }; const warnableUpdates = getWarnable(this.inProgressUpdates.values()); if (warnableUpdates.length > 0) { logs_1.log.warn(makeUnfinishedUpdateHandlerMessage(warnableUpdates)); } const warnableSignals = getWarnable(this.inProgressSignals.values()); if (warnableSignals.length > 0) { logs_1.log.warn(makeUnfinishedSignalHandlerMessage(warnableSignals)); } } updateRandomSeed(activation) { if (!activation.randomnessSeed) { throw new TypeError('Expected activation with randomnessSeed attribute'); } this.setRandomnessSeed(activation.randomnessSeed.toBytes()); } notifyHasPatch(activation) { if (!this.info.unsafe.isReplaying) throw new common_1.IllegalStateError('Unexpected notifyHasPatch job on non-replay activation'); if (!activation.patchId) throw new TypeError('notifyHasPatch missing patch id'); this.knownPresentPatches.add(activation.patchId); } patchInternal(patchId, deprecated) { if (this.workflow === undefined) { throw new common_1.IllegalStateError('Patches cannot be used before Workflow starts'); } const usePatch = !this.info.unsafe.isReplaying || this.knownPresentPatches.has(patchId); // Avoid sending commands for patches core already knows about. // This optimization enables development of automatic patching tools. if (usePatch && !this.sentPatches.has(patchId)) { this.pushCommand({ setPatchMarker: { patchId, deprecated }, }); this.sentPatches.add(patchId); } return usePatch; } /** * Called early while handling an activation to register known flags. * May be invoked from outside the VM. */ addKnownFlags(flags) { for (const flag of flags) { (0, flags_1.assertValidFlag)(flag); this.knownFlags.add(flag); } } /** * Check if an SDK Flag may be considered as enabled for the current Workflow Task. * * SDK flags play a role similar to the `patched()` API, but are meant for internal usage by the * SDK itself. They make it possible for the SDK to evolve its behaviors over time, while still * maintaining compatibility with Workflow histories produced by older SDKs, without causing * determinism violations. * * May be invoked from outside the VM. */ hasFlag(flag) { if (this.knownFlags.has(flag.id)) return true; // If not replaying, enable the flag if it is configured to be enabled by default. Setting a // flag's default to false allows progressive rollout of new feature flags, with the possibility // of reverting back to a version of the SDK where the flag is supported but disabled by default. // It is also useful for testing purpose. if (!this.info.unsafe.isReplaying && flag.default) { this.knownFlags.add(flag.id); return true; } // When replaying, a flag is considered enabled if it was enabled during the original execution of // that Workflow Task; this is normally determined by the presence of the flag ID in the corresponding // WFT Completed's `sdkMetadata.langUsedFlags`. // // SDK Flag Alternate Condition provides an alternative way of determining whether a flag should // be considered as enabled for the current WFT; e.g. by looking at the version of the SDK that // emitted a WFT. The main use case for this is to retroactively turn on some flags for WFT emitted // by previous SDKs that contained a bug. Alt Conditions should only be used as a last resort. // // Note that conditions are only evaluated while replaying. Also, alternate conditions will not // cause the flag to be persisted to the "used flags" set, which means that further Workflow Tasks // may not reflect this flag if the condition no longer holds. This is so to avoid incorrect // behaviors in case where a Workflow Execution has gone through a newer SDK version then again // through an older one. if (this.info.unsafe.isReplaying && flag.alternativeConditions) { for (const cond of flag.alternativeConditions) { if (cond({ info: this.info, sdkVersion: this.sdkVersion })) return true; } } return false; } removeFromCache() { throw new common_1.IllegalStateError('removeFromCache activation job should not reach workflow'); } /** * Transforms failures into a command to be sent to the server. * Used to handle any failure emitted by the Workflow. */ handleWorkflowFailure(error) { if (this.cancelled && (0, errors_1.isCancellation)(error)) { this.pushCommand({ cancelWorkflowExecution: {} }, true); } else if (error instanceof interfaces_1.ContinueAsNew) { this.pushCommand({ continueAsNewWorkflowExecution: error.command }, true); } else if (error instanceof common_1.TemporalFailure || this.isConfiguredFailureException(error)) { // Fail the workflow. We do not want to issue unfinishedHandlers warnings. To achieve that, we // mark all handlers as completed now. this.inProgressSignals.clear(); this.inProgressUpdates.clear(); this.pushCommand({ failWorkflowExecution: { failure: this.errorToFailure((0, common_1.ensureTemporalFailure)(error)), }, }, true); } else { this.recordWorkflowTaskError(error); } } /** * Returns true if the given error matches any of the configured failure exception types * (from {@link WorkerOptions.workflowFailureErrorTypes} or * {@link WorkflowDefinitionOptions.failureExceptionTypes}). */ isConfiguredFailureException(error) { // Check class references from WorkflowDefinitionOptions (instanceof-based, supports subclasses) if (this.workflowDefinitionFailureExceptionTypes) { // We guarantee that including Error in the list will catch _any_ error. if (this.workflowDefinitionFailureExceptionTypes.includes(Error)) return true; for (const errorType of this.workflowDefinitionFailureExceptionTypes) { if (error instanceof errorType) return true; } } // Check class name strings from WorkerOptions (prototype-chain-based) if (this.failureExceptionTypeNames.length > 0) { // We guarantee that including 'Error' in the list will catch _any_ error. if (this.failureExceptionTypeNames.includes('Error')) return true; if (typeof error === 'object' && error !== null) { let ctor = error.constructor; while (ctor != null && ctor !== Function.prototype) { const name = ctor.name; if (name) { if (this.failureExceptionTypeNames.includes(name)) return true; } ctor = Object.getPrototypeOf(ctor); } } } return false; } recordWorkflowTaskError(error) { // Only keep the first error that bubbles up; subsequent errors will be ignored. if (this.workflowTaskError === undefined) this.workflowTaskError = error; // Immediately rethrow the error if we know it is safe to do so (i.e. we are not running async // microtasks). Otherwise, the error will be rethrown whenever we get an opportunity to do so, // e.g. the next time `tryUnblockConditions()` is called. if (this.rethrowSynchronously) this.maybeRethrowWorkflowTaskError(); } /** * If a Workflow Task error was captured, and we are running in synchronous mode, * then bubble it up now. This is safe to call even if there is no error to rethrow. */ maybeRethrowWorkflowTaskError() { if (this.workflowTaskError) throw this.workflowTaskError; } completeQuery(queryId, result) { const context = this.workflowSerializationContext(); this.pushCommand({ respondToQuery: { queryId, succeeded: { response: this.payloadConverter.toPayload(result, context) } }, }); } failQuery(queryId, error) { this.pushCommand({ respondToQuery: { queryId, failed: this.errorToFailure((0, common_1.ensureTemporalFailure)(error)), }, }); } acceptUpdate(pr