UNPKG

@5minds/processcube_engine

Version:

The ProcessCube Engine. Stores and executes BPMNs.

708 lines • 51.8 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExecuteProcessService = void 0; const inversify_1 = require("inversify"); const processcube_engine_sdk_1 = require("@5minds/processcube_engine_sdk"); const index_1 = require("../Contracts/InternalMessages/index"); const IocRegistrations_1 = require("../Contracts/IocRegistrations"); const index_2 = require("../Tools/DatabaseAdaptersSequelize/index"); const EventAggregator_1 = __importDefault(require("../Tools/EventAggregator")); const IamService_1 = require("../Tools/Iam/IamService"); const IdentityService_1 = require("../Tools/Iam/IdentityService"); const ProcessDefinitionMediator_1 = require("../Tools/ProcessDefinitionMediator"); let ExecuteProcessService = class ExecuteProcessService { flowNodeInstanceDatabaseAdapter; processDefinitionMediator; processInstanceDatabaseAdapter; processInstanceFactory; iamService; identityService; logger; constructor(flowNodeInstanceRepository, processDefinitionMediator, processInstanceRepository, processInstanceFactory, iamService, identityService) { this.flowNodeInstanceDatabaseAdapter = flowNodeInstanceRepository; this.logger = new processcube_engine_sdk_1.Logger('execute_process_service'); this.processDefinitionMediator = processDefinitionMediator; this.processInstanceDatabaseAdapter = processInstanceRepository; this.processInstanceFactory = processInstanceFactory; this.iamService = iamService; this.identityService = identityService; } async start(identity, options) { return new Promise(async (resolve, reject) => { try { await this.validateStartRequest(identity, options); const processInstance = this.processInstanceFactory({ processInstanceId: options.processInstanceId }); // We must wait for the "ProcessStartedNotification", or this Use Case might resolve, before the Process Instance was actually created. const subscription = EventAggregator_1.default.subscribe(index_1.eventAggregatorSettings.messagePaths.processStarted, (message) => { if (message.processInstanceId === processInstance.getProcessInstanceId()) { EventAggregator_1.default.unsubscribe(subscription); resolve(message); } }); // This UseCase is designed to resolve immediately after the ProcessInstance was started, so we must not await the execution here. processInstance.execute(identity, options); } catch (error) { this.logger.error('An error occured during process execution!', { err: error, }); reject(error); } }); } async startAndAwaitEndEvent(identity, options) { await this.validateStartRequest(identity, options); const processInstance = this.processInstanceFactory({ processInstanceId: options.processInstanceId }); try { const executionPromise = processInstance.execute(identity, options); const eventSubscriptionPromise = this.waitForAnyEndEvent(processInstance); const results = await Promise.all([executionPromise, eventSubscriptionPromise]); return results[1]; } catch (error) { this.logger.error('An error occured during process execution!', { err: error, }); const isBpmnError = error.constructor?.name === processcube_engine_sdk_1.BpmnError.name; // Errors from the error provider and ErrorEndEvents are thrown as they are. // Everything else is thrown as an InternalServerError. if (isBpmnError || (0, processcube_engine_sdk_1.isEngineError)(error)) { throw error; } const errorToThrow = new processcube_engine_sdk_1.InternalServerError(error.message); errorToThrow.additionalInformation = { processInstanceId: processInstance.getProcessInstanceId(), correlationId: processInstance.getCorrelationId(), err: error, }; throw errorToThrow; } } async findAndResumeInterruptedProcessInstances() { await this.flowNodeInstanceDatabaseAdapter.cleanupOrphanedFlowNodeInstances(); this.logger.info('Resuming unfinished ProcessInstances.'); const runningProcessInstances = await this.processInstanceDatabaseAdapter.getMinimalRunningProcessInstances(); if (runningProcessInstances.length === 0) { this.logger.info(`No unfinished Process Instances found.`); return; } this.logger.info(`Found a grand total of ${runningProcessInstances.length} running Process Instances.`); const runningSubProcessInstances = runningProcessInstances.filter((processInstance) => processInstance.parentProcessInstanceId != undefined); if (runningSubProcessInstances.length > 0) { this.logger.info(`Skipping ${runningSubProcessInstances.length} Child Process Instances, which must be resumed through the Parent Process Instance.`); await this.finishOrphanedChildProcessIntances(runningSubProcessInstances, runningProcessInstances); } const resumableProcessInstancIds = runningProcessInstances.filter((processInstance) => !processInstance.parentProcessInstanceId).map((instance) => instance.processInstanceId); this.logger.info(`Found a grand total of ${resumableProcessInstancIds.length} resumable Process Instances.`); const resumableProcessInstances = await this.getBatchOfRunningInstances(resumableProcessInstancIds, resumableProcessInstancIds.length, 250); if (resumableProcessInstances.length === 0) { this.logger.info('No resumable ProcessInstances found.'); return; } this.logger.debug(`Starting resuming instances NOW...`); let resumedProcessInstanceCount = 0; for (const resumableInstance of resumableProcessInstances) { // Cannot await this, since all Process Instances need to be resumed simultaneously. this.resumeProcessInstance(resumableInstance).catch((error) => this.logger.error(`Resuming Process instance ${resumableInstance.processInstanceId} encountered an error: ${error?.message}`, { err: error })); resumedProcessInstanceCount++; if (resumedProcessInstanceCount % 150 === 0) { this.logger.info(`Resumed ${resumedProcessInstanceCount} of ${resumableProcessInstances.length} ProcessInstances.`); // Small buffer to prevent the RAM from exploding when resuming large amounts of processes await new Promise((resolve) => setTimeout(resolve, 250)); } } this.logger.info(`Finished resuming ${resumedProcessInstanceCount} ProcessInstances.`); } async retryProcessInstance(identity, processInstanceId, flowNodeInstanceId, newStartToken, newProcessDefinitionHash) { if (flowNodeInstanceId) { this.logger.info(`Attempting to retry ProcessInstance ${processInstanceId} at FlowNodeInstance ${flowNodeInstanceId}`); const flowNodeInstance = (await this.flowNodeInstanceDatabaseAdapter.query({ flowNodeInstanceId: flowNodeInstanceId })).flowNodeInstances[0]; if (!flowNodeInstance) { throw new processcube_engine_sdk_1.BadRequestError(`FlowNodeInstance with ID \`${flowNodeInstanceId}\` not found!`); } } else { this.logger.info(`Attempting to retry ProcessInstance ${processInstanceId}`); } const processInstanceToRetry = (await this.processInstanceDatabaseAdapter.getProcessInstances([processInstanceId]))[0]; if (!processInstanceToRetry) { throw new processcube_engine_sdk_1.NotFoundError(`ProcessInstance with ID \`${processInstanceId}\` not found.`); } const processInstanceNeedsMigration = newProcessDefinitionHash && newProcessDefinitionHash !== processInstanceToRetry.hash; if (processInstanceNeedsMigration) { await this.ensureProcessInstanceBpmnCanBeMigrated(identity, processInstanceToRetry, newProcessDefinitionHash, flowNodeInstanceId); } let rootProcessInstanceInTree = { ...processInstanceToRetry }; if (processInstanceToRetry.parentProcessInstanceId) { rootProcessInstanceInTree = await this.getRootProcessOfProcessInstanceTree(processInstanceToRetry); } const childProcessInstances = flowNodeInstanceId ? await this.processInstanceDatabaseAdapter.getProcessInstances(await this.processInstanceDatabaseAdapter.getAllDescendantProcessInstanceIds(rootProcessInstanceInTree.processInstanceId)) : await this.getRetryableChildProcessInstances([rootProcessInstanceInTree.processInstanceId]); const allProcessInstancesToReset = childProcessInstances.concat([rootProcessInstanceInTree]); const processInstanceSets = await Promise.all(allProcessInstancesToReset.map(async (processInstanceToReset) => { const queryResults = await this.flowNodeInstanceDatabaseAdapter.query({ processInstanceId: processInstanceToReset.processInstanceId }); const flowNodeInstances = queryResults.flowNodeInstances; return { processInstance: processInstanceToReset, flowNodeInstances: flowNodeInstances, }; })); const failedProcessInstanceSets = processInstanceSets.filter((processInstanceSet) => processInstanceSet.processInstance.state === processcube_engine_sdk_1.ProcessInstanceState.error || processInstanceSet.processInstance.state === processcube_engine_sdk_1.ProcessInstanceState.terminated); for (const processInstanceSet of failedProcessInstanceSets) { this.ensureProcessInstanceCanBeRestarted(processInstanceSet.processInstance); } if (flowNodeInstanceId) { await this.resetProcessInstanceToFlowNodeInstance(flowNodeInstanceId, processInstanceId, processInstanceSets); } else { await Promise.all(processInstanceSets.map(async (set) => { return this.resetProcessInstance(set.processInstance, set.flowNodeInstances); })); } if (processInstanceNeedsMigration) { await this.migrateProcessInstanceBpmn(processInstanceToRetry, newProcessDefinitionHash); if (rootProcessInstanceInTree.processInstanceId === processInstanceToRetry.processInstanceId) { // This is necessary to ensure that the process instance will use the new XML when restarting. // Only required when retrying a single instance, or when triggering the Retry at the top of of the Tree. rootProcessInstanceInTree = (await this.processInstanceDatabaseAdapter.getProcessInstances([processInstanceId]))[0]; } } if (newStartToken) { if (flowNodeInstanceId) { await this.flowNodeInstanceDatabaseAdapter.updateStartToken([flowNodeInstanceId], newStartToken); } else { const failedFlowNodeInstances = processInstanceSets.flatMap((pi) => pi.flowNodeInstances.filter((fni) => fni.state === processcube_engine_sdk_1.FlowNodeInstanceState.error || fni.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated)); const flowNodeInstancesToUpdateStartToken = failedFlowNodeInstances.filter((fni) => (fni.flowNodeType !== processcube_engine_sdk_1.BpmnType.callActivity || !childProcessInstances.some((pi) => pi.processInstanceId === fni.childProcessInstanceId)) && fni.flowNodeType !== processcube_engine_sdk_1.BpmnType.subProcess); const flowNodeInstanceIds = flowNodeInstancesToUpdateStartToken.map((fni) => fni.flowNodeInstanceId); await this.flowNodeInstanceDatabaseAdapter.updateStartToken(flowNodeInstanceIds, newStartToken); } } rootProcessInstanceInTree.state = processcube_engine_sdk_1.ProcessInstanceState.running; this.logger.info(`Resuming ProcessInstance with instance ID ${rootProcessInstanceInTree.processInstanceId} and Definition ID ${rootProcessInstanceInTree.processDefinitionId}`); // Must not await this, or the API request will hang until the Process Instance finishes. this.resumeProcessInstance(rootProcessInstanceInTree).catch((error) => this.logger.error(`Retrying Process instance ${rootProcessInstanceInTree.processInstanceId} encountered an error: ${error?.message}`, { err: error })); } /** * This function is too strict. It verifies, that all possible paths towards `flowNodeInstanceId` have not been altered. * It should only verify, that the actually executed path has not been altered. * https://5minds.atlassian.net/browse/PCE-1474 */ async ensureProcessInstanceBpmnCanBeMigrated(identity, processInstance, newProcessDefinitionHash, flowNodeInstanceId) { const flowNodeInstanceToRestartAt = (await this.flowNodeInstanceDatabaseAdapter.query({ flowNodeInstanceId: flowNodeInstanceId ?? undefined, processInstanceId: processInstance.processInstanceId, }, 0, 1, { sortBy: processcube_engine_sdk_1.FlowNodeInstanceSortableColumns.finishedAt, sortDir: 'DESC', })).flowNodeInstances[0]; if (!flowNodeInstanceToRestartAt) { throw new processcube_engine_sdk_1.BadRequestError(`Process Instance with ID \`${processInstance.processInstanceId}\` does not contain a FlowNodeInstance with ID \`${flowNodeInstanceId}\`.`); } const newProcessDefinition = await this.processDefinitionMediator.getByHash(identity, newProcessDefinitionHash); const newProcessModel = newProcessDefinition.processes.find((process) => process.id === processInstance.processModelId && process.isExecutable); if (!newProcessModel) { throw new processcube_engine_sdk_1.BadRequestError(`Process Model with ID \`${processInstance.processModelId}\` is no longer flagged as executable.`); } const newProcessModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(newProcessModel); const processDefinition = await this.processDefinitionMediator.getByHash(identity, processInstance.hash); const processModel = processDefinition.processes.find((process) => process.id === processInstance.processModelId); const processModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(processModel); const visitedFlowNodeIds = []; function compareSubProcessFlows(flowNode, newFlowNode) { const subProcessEndEvents = flowNode.flowNodes.filter((fn) => fn.bpmnType === processcube_engine_sdk_1.BpmnType.endEvent); const newSubProcessEndEvents = newFlowNode.flowNodes.filter((fn) => fn.bpmnType === processcube_engine_sdk_1.BpmnType.endEvent); if (subProcessEndEvents.length !== newSubProcessEndEvents.length) { return false; } const subProcessIsEqual = subProcessEndEvents.every((endEvent) => { const newEndEvent = newSubProcessEndEvents.find((ee) => ee.id === endEvent.id); if (!newEndEvent) { return false; } return compareFlows(endEvent, newEndEvent); }); return subProcessIsEqual; } function compareFlows(flowNode, newFlowNode) { if (visitedFlowNodeIds.includes(flowNode.id)) { return true; } visitedFlowNodeIds.push(flowNode.id); if (flowNode.id !== newFlowNode.id || flowNode.bpmnType !== newFlowNode.bpmnType) { return false; } if (flowNode.dataOutputAssociations?.length !== newFlowNode.dataOutputAssociations?.length || flowNode.dataOutputAssociations?.some((doa) => newFlowNode.dataOutputAssociations.findIndex((ndoa) => ndoa.id === doa.id && ndoa.targetRef === doa.targetRef) == -1)) { return false; } if (flowNode.bpmnType === processcube_engine_sdk_1.BpmnType.subProcess) { const subProcessIsEqual = compareSubProcessFlows(flowNode, newFlowNode); if (!subProcessIsEqual) { return false; } } const precedingFlowNodes = processModelFacade.getPreviousFlowNodesFor(flowNode, false, true) ?? []; const newPrecedingFlowNodes = newProcessModelFacade.getPreviousFlowNodesFor(newFlowNode, false, true) ?? []; if (precedingFlowNodes.length !== newPrecedingFlowNodes.length) { return false; } const precedingFlowsAreEqual = precedingFlowNodes.every((flowNode) => { const newFlowNode = newPrecedingFlowNodes.find((newFlowNode) => newFlowNode.id === flowNode.id); if (newFlowNode) { return compareFlows(flowNode, newFlowNode); } return false; }); return precedingFlowsAreEqual; } const newFlowNodeToRestartAt = newProcessModelFacade.getFlowNodeById(flowNodeInstanceToRestartAt.flowNodeId, true); if (!newFlowNodeToRestartAt) { throw new processcube_engine_sdk_1.BadRequestError(`Process Definition with ID \`${processInstance.processDefinitionId}\` does not contain a FlowNode with ID \`${flowNodeInstanceToRestartAt.flowNodeId}\`.`); } const flowNodeToRestartAt = processModelFacade.getFlowNodeById(flowNodeInstanceToRestartAt.flowNodeId, true); if (!compareFlows(flowNodeToRestartAt, newFlowNodeToRestartAt)) { throw new processcube_engine_sdk_1.BadRequestError(`Can't update Process Model for Process Instance \`${flowNodeInstanceToRestartAt.processInstanceId}\`, because the path before Flow Node \`${flowNodeInstanceToRestartAt.flowNodeId}\` or some DataOutputAssociation has been altered.`); } } async migrateProcessInstanceBpmn(processInstance, newProcessDefinitionHash) { const rootProcessInstance = await this.getRootProcessOfProcessInstanceTree(processInstance, true); const allEffectedProcessInstanceIds = await this.processInstanceDatabaseAdapter.getAllDescendantProcessInstanceIds(rootProcessInstance.processInstanceId, processcube_engine_sdk_1.BpmnType.subProcess); allEffectedProcessInstanceIds.push(rootProcessInstance.processInstanceId); await Promise.all(allEffectedProcessInstanceIds.map((pi) => this.processInstanceDatabaseAdapter.changeProcessDefinition(pi, newProcessDefinitionHash))); const newProcessDefinition = await this.processDefinitionMediator.getByHash(this.identityService.getInternalIdentity(), newProcessDefinitionHash); const newProcessModel = newProcessDefinition.processes.find((process) => process.id === processInstance.processModelId && process.isExecutable); const newProcessModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(newProcessModel); const executedFlowNodes = await this.flowNodeInstanceDatabaseAdapter.getFlowNodeInstancesForProcessInstanceIds(allEffectedProcessInstanceIds); const flowNodesToUpdate = newProcessModel .getAllFlowNodes() .map((flowNode) => { return { ...flowNode, lane: newProcessModelFacade.getLaneForFlowNode(flowNode.id)?.name ?? null, }; }) .filter((flowNode) => { const executedFlowNode = executedFlowNodes.find((executedFlowNode) => executedFlowNode.flowNodeId === flowNode.id); return executedFlowNode != null && (executedFlowNode.flowNodeName != flowNode.name || executedFlowNode.flowNodeLane != flowNode.lane); }); await this.flowNodeInstanceDatabaseAdapter.updateFlowNodesInProcessInstance(allEffectedProcessInstanceIds, flowNodesToUpdate); } async validateStartRequest(requestingIdentity, options) { if (!options?.processModelId) { throw new processcube_engine_sdk_1.BadRequestError('Must provide a value for options.processModelId!'); } const processDefinition = await this.processDefinitionMediator.getByProcessModelId(requestingIdentity, options.processModelId); const processModel = processDefinition?.processes?.find((process) => process.id === options.processModelId); if (!processModel) { throw new processcube_engine_sdk_1.NotFoundError(`ProcessModel \`${options.processModelId}\` not found.`); } if (processModel.dataObjectReferences.length > 0) { processModel.dataObjectReferences.forEach((dataObjectReference) => { if (dataObjectReference.initialValue == undefined) { return; } let initialValue; try { initialValue = JSON.parse(dataObjectReference.initialValue); } catch { } const resultIsAnObject = initialValue != undefined && typeof initialValue === 'object' && (initialValue.serialize || initialValue.toString() === '[object Object]' || Array.isArray(initialValue)); if (!resultIsAnObject) { throw new processcube_engine_sdk_1.BadRequestError(`The given initial value for data object ${dataObjectReference.id} is not a valid JSON.`); } }); } const processModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(processModel); const startEvents = processModelFacade.getStartEvents(false); const endEvents = processModelFacade.getEndEvents(false); if (startEvents.length > 1 && !options.startEventId) { throw new processcube_engine_sdk_1.BadRequestError('Must select a StartEvent with which to start the Process.'); } if (options.startEventId != undefined) { const matchingStartEvent = startEvents.find((flowNode) => flowNode.id === options.startEventId); if (!matchingStartEvent) { throw new processcube_engine_sdk_1.NotFoundError(`StartEvent with ID \`${options.startEventId}\` not found.`); } } const startEventToUse = options.startEventId ? processModelFacade.getStartEventById(options.startEventId) : processModelFacade.getSingleStartEvent(); const userIsAdmin = this.iamService.checkIfUserIsSuperAdmin(requestingIdentity); const userIsObserver = this.iamService.checkIfUserIsObserver(requestingIdentity); const flowNodeLane = processModelFacade.getLaneForFlowNode(startEventToUse.id)?.name; if (!userIsAdmin && userIsObserver && flowNodeLane && !this.iamService.checkIfIdentityHasClaim(requestingIdentity, flowNodeLane)) { const forbiddenError = new processcube_engine_sdk_1.ForbiddenError(`Observers are not allowed to start processes without the corresponding lane claim.`); forbiddenError.additionalInformation = { processModelId: options.processModelId, startEventId: startEventToUse.id, requestingUserId: requestingIdentity.userId, requestingUserEmail: requestingIdentity.userEmail, requestingUserName: requestingIdentity.userName, }; throw forbiddenError; } if (options.endEventId) { const noMatchingEndEvent = !endEvents.some((flowNode) => flowNode.id === options.endEventId); if (noMatchingEndEvent) { throw new processcube_engine_sdk_1.NotFoundError(`EndEvent with ID \`${options.endEventId}\` not found.`); } } if (!processModel.isExecutable) { throw new processcube_engine_sdk_1.BadRequestError(`The process model \`${processModel.id}\` is not executable.`); } if (processModel.isSingleton) { const singletonProcessInstances = await this.processInstanceDatabaseAdapter.countRunningInstancesForModel(processModel.id); if (singletonProcessInstances > 0) { throw new processcube_engine_sdk_1.BadRequestError(`The ProcessModel \`${options.processModelId}\` is a Singleton and already running in instance \`${singletonProcessInstances[0]}\``); } } } async waitForAnyEndEvent(processInstance) { return new Promise((resolve) => processInstance.onProcessEnded(resolve)); } async getBatchOfRunningInstances(resumableInstanceIds, totalCount, chunkSize = 250) { const processInstanceIdChunk = resumableInstanceIds.splice(0, chunkSize); const loadedProcessInstances = await this.processInstanceDatabaseAdapter.getProcessInstances(processInstanceIdChunk); if (resumableInstanceIds.length > 0) { return loadedProcessInstances.concat(await this.getBatchOfRunningInstances(resumableInstanceIds, totalCount, chunkSize)); } else { return loadedProcessInstances; } } async getRootProcessOfProcessInstanceTree(processInstance, onlyEmbedded = false) { if (!processInstance.parentProcessInstanceId || (onlyEmbedded && !processInstance.embeddedProcessModelId)) { return processInstance; } const parentProcessInstances = await this.processInstanceDatabaseAdapter.getProcessInstances([processInstance.parentProcessInstanceId]); if (parentProcessInstances?.length === 0) { throw new processcube_engine_sdk_1.NotFoundError(`Parent ProcessInstance \`${processInstance.parentProcessInstanceId}\` of child \`${processInstance.processInstanceId}\` not found.`); } const parentProcessInstance = parentProcessInstances[0]; return this.getRootProcessOfProcessInstanceTree(parentProcessInstance, onlyEmbedded); } async getRetryableChildProcessInstances(parentProcessInstanceIds) { const childProcessInstances = await this.processInstanceDatabaseAdapter.getRetryableChildProcessInstances(parentProcessInstanceIds); if (childProcessInstances.length === 0) { return []; } const childIds = childProcessInstances.map((instance) => instance.processInstanceId); const childResults = await this.getRetryableChildProcessInstances(childIds); return childProcessInstances.concat(childResults); } async resetProcessInstanceToFlowNodeInstance(flowNodeInstanceId, processInstanceId, processInstanceSets) { const processInstanceSetToReset = processInstanceSets.find((piSet) => piSet.processInstance.processInstanceId === processInstanceId); const processInstance = processInstanceSetToReset.processInstance; const flowNodeInstances = processInstanceSetToReset.flowNodeInstances; const flowNodeInstance = flowNodeInstances.find((fniEntry) => fniEntry.flowNodeInstanceId === flowNodeInstanceId); if (flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.parallelGateway || flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.inclusiveGateway || flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.exclusiveGateway || flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.eventBasedGateway || flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.complexGateway || flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.boundaryEvent) { throw new processcube_engine_sdk_1.BadRequestError(`Resetting a ProcessInstance to a ${flowNodeInstance.flowNodeType.replace('bpmn:', '')} is currently not supported.`); } const bpmnModelParser = new processcube_engine_sdk_1.BpmnModelParser(); const processDefinition = await bpmnModelParser.parseXmlToObjectModel(processInstance.xml); const process = processDefinition.processes.find((processEntry) => processEntry.id === flowNodeInstance.processModelId); const previousParallelGateways = this.getPreviousParallelizingGateways(flowNodeInstanceId, flowNodeInstances); const isRunningInParallel = this.isFlowNodeInParallizedBranch(process, previousParallelGateways); if (isRunningInParallel) { throw new processcube_engine_sdk_1.BadRequestError(`Resetting to Flow Node Instances running in parallel branches is currently not supported.`); } const flowNodeInstancesToDelete = await this.getFlowNodeInstancesAfterFlowNodeInstance(flowNodeInstance, flowNodeInstances, process); const processInstanceIdsToDelete = this.getAllChildProcessIds(flowNodeInstancesToDelete, processInstanceSets); const setsToCompletelyReset = this.getNonDescendantProcessInstanceSets(processInstanceSetToReset, processInstanceSets); if (setsToCompletelyReset.length > 0) { await Promise.all(setsToCompletelyReset.map(async (set) => { return this.resetProcessInstance(set.processInstance, set.flowNodeInstances); })); } if (flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.callActivity) { processInstanceIdsToDelete.push(flowNodeInstance.childProcessInstanceId); } else if (flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess) { processInstanceIdsToDelete.push(flowNodeInstance.childProcessInstanceId); } const flowNodeInstanceIdsToReset = []; if (flowNodeInstance.flowNodeInstanceId === flowNodeInstanceId && flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess) { flowNodeInstanceIdsToReset.push({ flowNodeInstanceId: flowNodeInstance.flowNodeInstanceId, isUnstartedSubProcess: true }); } else { flowNodeInstanceIdsToReset.push(flowNodeInstance.flowNodeInstanceId); } flowNodeInstancesToDelete.push(...this.getBoundaryEventsForFlowNodeInstances([...flowNodeInstancesToDelete, flowNodeInstance], flowNodeInstances)); const flowNodeInstanceIdsToDelete = flowNodeInstancesToDelete.map((fni) => fni.flowNodeInstanceId); this.logger.debug(`Resetting Process Instance ${processInstanceId} will delete the following Flow Node Instances:`, { flowNodeInstancesToDelete: flowNodeInstanceIdsToDelete }); await this.processInstanceDatabaseAdapter.reset(processInstance.processInstanceId, flowNodeInstanceIdsToReset, flowNodeInstanceIdsToDelete, processInstanceIdsToDelete); } getNonDescendantProcessInstanceSets(processInstanceSet, processInstanceSets) { const processInstanceIdBlacklist = [processInstanceSet.processInstance.processInstanceId]; let nonDescendantSets = [...processInstanceSets]; let nonDescendantSetsLength = 0; while (nonDescendantSetsLength !== nonDescendantSets.length) { nonDescendantSetsLength = nonDescendantSets.length; nonDescendantSets = nonDescendantSets.filter((set) => { if (processInstanceIdBlacklist.includes(set.processInstance.processInstanceId)) { return false; } else if (processInstanceIdBlacklist.includes(set.processInstance.parentProcessInstanceId)) { processInstanceIdBlacklist.push(set.processInstance.processInstanceId); return false; } return true; }); } return nonDescendantSets; } getAllChildProcessIds(flowNodeInstances, processInstanceSets) { const processInstanceIds = flowNodeInstances .filter((fni) => fni.flowNodeType === processcube_engine_sdk_1.BpmnType.callActivity || fni.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess) .map((fni) => fni.childProcessInstanceId); const processInstanceSetsForChildProcesses = processInstanceSets.filter((processInstanceSet) => processInstanceIds.includes(processInstanceSet.processInstance.processInstanceId)); for (const processInstanceSet of processInstanceSetsForChildProcesses) { const childProcessIds = this.getAllChildProcessIds(processInstanceSet.flowNodeInstances, processInstanceSets); processInstanceIds.push(...childProcessIds); } return processInstanceIds; } isFlowNodeInParallizedBranch(process, gatewayInstances) { const processModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(process); const parallelGatewayModels = process.getFlowNodesByType(processcube_engine_sdk_1.BpmnType.parallelGateway); const inclusiveGatewayModels = process.getFlowNodesByType(processcube_engine_sdk_1.BpmnType.inclusiveGateway); const gatewayModels = parallelGatewayModels.concat(inclusiveGatewayModels); const usedGatewayModels = gatewayModels.filter((gw) => gatewayInstances.some((gwi) => gwi.flowNodeId === gw.id)); const splitGateways = usedGatewayModels.filter((gw) => gw.gatewayDirection === processcube_engine_sdk_1.Model.Gateways.GatewayDirection.Diverging); const splitGatewayInstances = gatewayInstances.filter((gwi) => splitGateways.some((gw) => gwi.flowNodeId === gw.id)); for (const splitGatewayInstance of splitGatewayInstances) { const splitGatewayModel = splitGateways.find((gw) => gw.id === splitGatewayInstance.flowNodeId); const joinGateway = processModelFacade.findJoinGatewayAfterSplitGateway(splitGatewayModel, splitGatewayModel.bpmnType); if (!joinGateway) { return true; } const joinGatewayInstance = gatewayInstances.find((gwi) => gwi.flowNodeId === joinGateway.id); if (!joinGatewayInstance || joinGatewayInstance.state !== processcube_engine_sdk_1.FlowNodeInstanceState.finished) { return true; } } return false; } getPreviousParallelizingGateways(flowNodeInstanceId, allFlowNodeInstances) { const flowNodeInstance = allFlowNodeInstances.find((fni) => fni.flowNodeInstanceId === flowNodeInstanceId); if (!flowNodeInstance.previousFlowNodeInstanceId) { return []; } const gateways = []; if (flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.parallelGateway || flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.inclusiveGateway) { gateways.push(flowNodeInstance); } const previousFlowNodeInstanceIds = flowNodeInstance.previousFlowNodeInstanceId.split(';'); for (const previousFlowNodeInstanceId of previousFlowNodeInstanceIds) { gateways.push(...this.getPreviousParallelizingGateways(previousFlowNodeInstanceId, allFlowNodeInstances)); } return gateways; } ensureProcessInstanceCanBeRestarted(processInstance) { if (processInstance.state !== processcube_engine_sdk_1.ProcessInstanceState.error && processInstance.state !== processcube_engine_sdk_1.ProcessInstanceState.terminated) { const errorMessage = `ProcessInstance with ID ${processInstance.processInstanceId} can not be restarted, because it is not in an 'error' or 'terminated' state`; const error = new processcube_engine_sdk_1.BadRequestError(errorMessage); this.logger.error(errorMessage, { err: error, }); throw error; } } async getFlowNodeInstancesAfterFlowNodeInstance(flowNodeInstance, allFlowNodeInstances, processModel) { const flowNodeInstancesAfterFlowNodeInstance = allFlowNodeInstances.filter((fni) => { if (fni.previousFlowNodeInstanceId === flowNodeInstance.flowNodeInstanceId) { return true; } if (fni.previousFlowNodeInstanceId?.includes(';')) { return fni.previousFlowNodeInstanceId.split(';').some((previousFlowNodeInstanceId) => previousFlowNodeInstanceId === flowNodeInstance.flowNodeInstanceId); } return false; }); const flowNodeInstanceIsSubProcess = flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess; if (flowNodeInstancesAfterFlowNodeInstance.length === 0 && !flowNodeInstanceIsSubProcess) { return []; } const flowNodeInstancesToReset = [...flowNodeInstancesAfterFlowNodeInstance]; for (const flowNodeInstanceEntry of flowNodeInstancesAfterFlowNodeInstance) { const flowNodeInstanceAfterCurrentFlowNodeInstance = await this.getFlowNodeInstancesAfterFlowNodeInstance(flowNodeInstanceEntry, allFlowNodeInstances, processModel); flowNodeInstancesToReset.push(...flowNodeInstanceAfterCurrentFlowNodeInstance); } const subProcessInstancesToReset = flowNodeInstancesAfterFlowNodeInstance.filter((fni) => fni.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess && (fni.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated || fni.state === processcube_engine_sdk_1.FlowNodeInstanceState.error)); if (flowNodeInstanceIsSubProcess) { subProcessInstancesToReset.push(flowNodeInstance); } if (subProcessInstancesToReset.length > 0) { const followingFlowNodeInstancesForSubProcess = await this.getFollowingFlowNodeInstancesForSubProcesses(subProcessInstancesToReset, processModel); flowNodeInstancesToReset.push(...followingFlowNodeInstancesForSubProcess); } return flowNodeInstancesToReset; } async getFollowingFlowNodeInstancesForSubProcesses(subProcessInstances, processModel) { const childFlowNodeInstances = (await this.flowNodeInstanceDatabaseAdapter.query({ processInstanceId: subProcessInstances.map((spi) => spi.childProcessInstanceId) })).flowNodeInstances; const processModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(processModel); return (await Promise.all(subProcessInstances.map((spi) => this.getFollowingFlowNodeInstancesForSubProcess(spi, childFlowNodeInstances, processModelFacade, processModel)))).flat(); } async getFollowingFlowNodeInstancesForSubProcess(subProcessInstance, childFlowNodeInstances, processModelFacade, processModel) { const subProcessActivity = processModelFacade.getFlowNodeById(subProcessInstance.flowNodeId, true); const startEventIds = subProcessActivity.flowNodes.filter((fn) => fn.bpmnType === processcube_engine_sdk_1.BpmnType.startEvent).map((se) => se.id); const startEventInstancesForSubProcessModel = childFlowNodeInstances.filter((fni) => startEventIds.includes(fni.flowNodeId)); let startEventInstance; if (startEventInstancesForSubProcessModel.length > 1) { startEventInstance = startEventInstancesForSubProcessModel.sort(function (a, b) { const distanceA = Math.abs(subProcessInstance.startedAt.getTime() - a.startedAt.getTime()); const distanceB = Math.abs(subProcessInstance.startedAt.getTime() - b.startedAt.getTime()); return distanceA - distanceB; })[0]; } else if (startEventInstancesForSubProcessModel.length === 1) { startEventInstance = startEventInstancesForSubProcessModel[0]; } const followingFlowNodeInstancesForSubProcess = await this.getFlowNodeInstancesAfterFlowNodeInstance(startEventInstance, childFlowNodeInstances, processModel); followingFlowNodeInstancesForSubProcess.push(startEventInstance); return followingFlowNodeInstancesForSubProcess; } async resetProcessInstance(processInstance, flowNodeInstances) { const flowNodeInstancesToReset = await this.getFlowNodeInstancesToReset(flowNodeInstances, processInstance.processInstanceId); const flowNodeInstanceIdsToReset = flowNodeInstancesToReset.map((flowNodeInstance) => flowNodeInstance.flowNodeInstanceId); await this.processInstanceDatabaseAdapter.reset(processInstance.processInstanceId, flowNodeInstanceIdsToReset); } async getFlowNodeInstancesToReset(flowNodeInstances, processInstanceId) { const erroredFlowNodeInstances = flowNodeInstances.filter((flowNodeInstance) => flowNodeInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.error || flowNodeInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated); const flowNodeInstancesToReset = erroredFlowNodeInstances.filter((flowNodeInstance) => this.shouldResetFlowNode(flowNodeInstance, flowNodeInstances)); const failedSubProcesses = flowNodeInstancesToReset.filter((flowNodeInstance) => flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess); if (failedSubProcesses.length > 0) { flowNodeInstancesToReset.push(...(await this.getFlowNodeInstanceIdsInSubProcesses(processInstanceId))); } flowNodeInstancesToReset.push(...this.getBoundaryEventsForFlowNodeInstances(flowNodeInstancesToReset, flowNodeInstances)); return flowNodeInstancesToReset; } async getFlowNodeInstanceIdsInSubProcesses(processInstanceId) { const failedFlowNodeInstancesInFailedSubProcesses = await this.flowNodeInstanceDatabaseAdapter.query({ parentProcessInstanceId: processInstanceId, state: [processcube_engine_sdk_1.FlowNodeInstanceState.error, processcube_engine_sdk_1.FlowNodeInstanceState.terminated], }); return failedFlowNodeInstancesInFailedSubProcesses.flowNodeInstances; } getBoundaryEventsForFlowNodeInstances(flowNodeInstances, allFlowNodeInstances) { const boundaryEventsForFlowNodeInstances = flowNodeInstances.flatMap((flowNodeInstance) => { return allFlowNodeInstances.filter((possibleBoundaryEvent) => { return possibleBoundaryEvent.flowNodeType === processcube_engine_sdk_1.BpmnType.boundaryEvent && possibleBoundaryEvent.previousFlowNodeInstanceId === flowNodeInstance.multiInstanceMetadataId; }); }); return boundaryEventsForFlowNodeInstances; } shouldResetFlowNode(flowNodeInstanceToReset, flowNodeInstances) { const errorBoundaryEventInstances = this.getErrorBoundaryEventInstancesForFlowNodeInstance(flowNodeInstanceToReset, flowNodeInstances); const errorWasHandledByBoundaryEvent = errorBoundaryEventInstances.some((boundaryEventInstance) => { return boundaryEventInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.finished; }); return !errorWasHandledByBoundaryEvent; } getErrorBoundaryEventInstancesForFlowNodeInstance(flowNodeInstance, allFlowNodeInstances) { return allFlowNodeInstances.filter((possibleBoundaryEvent) => { return (possibleBoundaryEvent.eventType === processcube_engine_sdk_1.EventType.errorEvent && possibleBoundaryEvent.flowNodeType === processcube_engine_sdk_1.BpmnType.boundaryEvent && possibleBoundaryEvent.previousFlowNodeInstanceId === flowNodeInstance.flowNodeInstanceId); }); } async resumeProcessInstance(processInstance) { const flowNodeInstances = (await this.flowNodeInstanceDatabaseAdapter.query({ processInstanceId: processInstance.processInstanceId })).flowNodeInstances; const hasActiveFlowNodeInstances = flowNodeInstances.some((entry) => { return entry.state === processcube_engine_sdk_1.FlowNodeInstanceState.running || entry.state === processcube_engine_sdk_1.FlowNodeInstanceState.suspended; }); const lastFlowNodeInstance = flowNodeInstances.sort((a, b) => { if (a.startedAt && b.startedAt) { return a.startedAt > b.startedAt ? -1 : b.startedAt > a.startedAt ? 1 : 0; } if (a.startedAt == b.startedAt) { return b.flowNodeInstanceId.localeCompare(a.flowNodeInstanceId); } })[0]; const lastInstanceHasFailed = lastFlowNodeInstance != null && (lastFlowNodeInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.error || lastFlowNodeInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated); const hasReachedAnEndEvent = flowNodeInstances.some((entry) => entry.flowNodeType === processcube_engine_sdk_1.BpmnType.endEvent); const processInstanceIsOrphaned = (!hasActiveFlowNodeInstances && hasReachedAnEndEvent) || lastInstanceHasFailed; if (processInstanceIsOrphaned) { this.logger.warn(`ProcessInstance ${processInstance.processInstanceId} is not active anymore. It is likely something went wrong during final state transition.`); this.logger.warn(`Setting orphaned ProcessInstance ${processInstance.processInstanceId} state to "finished", so it won't show up again.`); return this.finishOrphanedProcessInstance(flowNodeInstances, processInstance); } try { const newProcessInstance = this.processInstanceFactory({ processInstanceId: processInstance.processInstanceId }); const ownerIdentity = processInstance.startedByRootAccessToken ? this.identityService.getRootAccessIdentity() : await this.identityService.getIdentity({ token: processInstance.ownerToken, userId: processInstance.ownerId, skipValidation: true, }); await newProcessInstance.resume(ownerIdentity, processInstance, flowNodeInstances); } catch (error) { // Errors from our error provider and ErrorEndEvents are thrown as they are. // Everything else is thrown as an InternalServerError. const isPresetError = error.code && error.name; if (isPresetError) { throw error; } else { throw new processcube_engine_sdk_1.InternalServerError(error.message); } } } async finishOrphanedChildProcessIntances(runningSubProcessInstances, runningProcessInstances) { this.logger.info('Checking for orphaned Child Process Instances...'); for (const subProcessInstance of runningSubProcessInstances) { const parentIsActive = runningProcessInstances.some((processInstance) => processInstance.processInstanceId === subProcessInstance.parentProcessInstanceId); if (parentIsActive) { continue; } this.logger.warn(`Parent of running ProcessInstance ${subProcessInstance.processInstanceId} has finished. It is likely something went wrong during final state transition.`); this.logger.warn(`Setting orphaned Child ProcessInstance ${subProcessInstance.processInstanceId} state to "terminated", so it won't show up again.`); const terminationError = new processcube_engine_sdk_1.BadRequestError(`Process was terminated, because its parent is no longer active.`); terminationError.additionalInformation = { processInstanceWasTerminated: true, }; await this.processInstanceDatabaseAdapter.terminate(subProcessInstance.processInstanceId, terminationError, undefined, true); } } async finishOrphanedProcessInstance(flowNodeInstances, processInstance) { const finalFlowNodeInstance = this.getFinalFlowNodeInstanceForOrphanedProcessInstance(flowNodeInstances); const finalToken = finalFlowNodeInstance?.endToken ?? {}; const processFinishedWithError = finalFlowNodeInstance?.state === processcube_engine_sdk_1.FlowNodeInstanceState.error || finalFlowNodeInstance?.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated; if (processFinishedWithError) { const errorToUse = finalFlowNodeInstance.error ?? new processcube_engine_sdk_1.BadRequestError('Process was terminated.'); errorToUse.additionalInformation = { processInstanceWasTerminated: true, }; await this.processInstanceDatabaseAdapter.finishWithError(processInstance.processInstanceId, errorToUse, undefined, true); } else { await this.processInstanceDatabaseAdapter.finish(processInstance.processInstanceId, { endEventId: finalFlowNodeInstance?.flowNodeId, endEventType: finalFlowNodeInstance?.eventType, endEventToken: finalToken, }, true); } } getFinalFlowNodeInstanceFor