Source: engine/universal/decider/module.js

const constraintManager = require('./constraintManager');
const Hceval = require('./hard_constraint_evaluation/hc-eval.js');
const Sceval = require('./soft_constraint_evaluation/sc-eval.js');
const routes = require('./deciderRoutes.js');
const { config } = require('@proceed/machine');

/**
 * This Module evaluates constaints and finds the next fitting Machines
 * @module @proceed/decider
 * @see https://gitlab.com/dBPMS-PROCEED/proceed/-/wikis/Engine/Universal/Process/Decision-Making#architecture-of-decider
 *
 */
const decider = {
  /**
   * Function to decide after each flow node which Machine is the optimal one for the next execution
   * @see  {@link https://gitlab.com/dBPMS-PROCEED/proceed/-/wikis/Engine/Universal/Process/Decision-Making#architecture-of-decider|Wiki Step 1 and 5}
   *
   * @param {Object} processInfo - Infos about the current running Process Model
   * @param {string} processInfo.id - the definitions ID of the current process
   * @param {string} processInfo.name - the definitions Name value of the current process
   * @param {string} processInfo.creatorEnvId - the ID of the Environment where the process where created
   * @param {string} processInfo.creatorEnvName - the Name of the Environment where the process where created
   * @param {Object} processInfo.nextFlowNode - information about next BPMN element, the one this method tries to find the optimal machine for
   * @param {string} processInfo.nextFlowNode.id - the ID of the next BPMN element
   * @param {boolean} processInfo.nextFlowNode.isUserTask - indicates if next flow Node is a usertask
   *
   * @param {Object} token - Infos about the current running process instance, but contains only one token of the current flow (see {@link https://gitlab.com/dBPMS-PROCEED/proceed/-/wikis/Engine/Universal/Process/Process-Instance,-Tokens,-Datastructure-and-REST-Endpoint#the-instance|Wiki Instance Description}), at least:
   * @param {Date} token.globalStartTime - Time when the Instance was started globally
   * @param {Date} token.localStartTime - Time and date of the token start (on the local Engine)
   * @param {Date} token.localExecutionTime - Relative time in seconds a token has already needed for executing the BPMN elements
   * @param {number} token.machineHops - The number of Machines a token was already on (hopping)
   *
   * @param {flowNodeConstraints} - Contains the parsed JS-version from inside \<proceed:processConstraints\> of the next Flow Node
   * @param {processConstraints} - Contains the parsed JS-version from inside \<proceed:processConstraints\> of the complete Process
   *
   * @returns {MachineResultObject} Returns a list of engines in an object, see the type definition below
   */
  async findOptimalNextMachine(processInfo, token, flowNodeConstraints, processConstraints) {
    if (!processInfo || !token) {
      throw new Error(
        "Missing input parameter for method 'findOptimalNextMachine' in Decider Module"
      );
    }
    /**
     * Result object that contains the list of possible Machines
     *
     * @typedef {Object} MachineResultObject
     * @property {Array} engineList - List of engines
     * @property {boolean} prioritized - Indicates if the engine list is prioritized, -> means the best fit at the first index
     * @property {object} abortCheck - Indicating if token or process has to be aborted and naming unfulfilled constraint
     * @property {String} abortCheck.stopProcess - Wether token or process has to be aborted
     * @property {Array} abortCheck.unfulfilledConstraints - List of unfulfilled constraints for aborted token/process
     */

    const machineResultObject = {
      engineList: [],
      prioritized: false,
      abortCheck: {
        stopProcess: null, // "token"|"instance"|null
        unfulfilledConstraints: [],
      },
    };

    const preCheckAbortResult = await this.preCheckAbort(
      processInfo,
      token,
      flowNodeConstraints.hardConstraints || [],
      processConstraints.hardConstraints || []
    );

    if (preCheckAbortResult.stopProcess !== null) {
      machineResultObject.abortCheck = preCheckAbortResult;
      return machineResultObject;
    }

    const hardConstraints = constraintManager.concatAllConstraints(
      flowNodeConstraints.hardConstraints,
      processConstraints.hardConstraints
    );

    const softConstraints = constraintManager.concatAllConstraints(
      flowNodeConstraints.softConstraints,
      processConstraints.softConstraints
    );

    const processExecutionConstraintNames = [
      'maxTime',
      'maxTimeGlobal',
      'maxTokenStorageTime',
      'maxTokenStorageRounds',
      'maxMachineHops',
      'sameMachine',
    ];

    const remainingConstraints = hardConstraints.filter(
      (hardConstraint) => !processExecutionConstraintNames.includes(hardConstraint.name)
    );

    // Step 2.2, if false: does not have to be carried only locally
    if (await constraintManager.preCheckLocalExec(hardConstraints)) {
      const localExecAllowed =
        remainingConstraints.length === 0 ||
        (await Hceval.machineSatisfiesAllHardConstraints(remainingConstraints));

      if (localExecAllowed) {
        machineResultObject.engineList.push('local-engine');
      }

      return machineResultObject;
    }

    // Step 2.3 + 2.4 + 2.5
    const valuesList = await constraintManager.getSoftConstraintValues(
      remainingConstraints,
      softConstraints,
      processInfo.nextFlowNode
    );

    // Step 3
    const evaluatedMachines = Sceval.evaluateEveryMachine(softConstraints, valuesList);

    machineResultObject.engineList = evaluatedMachines;

    if (softConstraints.length > 0) {
      machineResultObject.prioritized = true;
    }

    return machineResultObject;
  },

  /**
   * Function to check if a process can be executed locally
   * (after receiving and before starting a process)
   * Skips steps 3.3 and 3.6
   * @see  {@link https://gitlab.com/dBPMS-PROCEED/proceed/-/wikis/Engine/Universal/Process/Decision-Making#architecture-of-decider|Wiki Step 1 and 5}
   *
   * @param {Object} processInfo - see {@link findOptimalNextMachine()}
   * @param {Object} token - see {@link findOptimalNextMachine()}
   * @param {flowNodeConstraints} - see {@link findOptimalNextMachine()}
   * @param {processConstraints} - see {@link findOptimalNextMachine()}
   * @returns {boolean} Returns wether the process is locally executeable
   */
  async allowedToExecuteLocally(processInfo, token, flowNodeConstraints, processConstraints) {
    if (!processInfo) {
      throw new Error(
        "Missing input parameter for method 'allowedToExecuteLocally' in Decider Module"
      );
    }

    if (token) {
      const preCheckAbortResult = await this.preCheckAbort(
        processInfo,
        token,
        flowNodeConstraints.hardConstraints || [],
        processConstraints.hardConstraints || []
      );

      if (preCheckAbortResult.stopProcess !== null) {
        return false;
      }
    }

    if (!flowNodeConstraints.hardConstraints && !processConstraints.hardConstraints) {
      return true;
    }

    const hardConstraints = constraintManager.concatAllConstraints(
      flowNodeConstraints.hardConstraints,
      processConstraints.hardConstraints
    );

    const processExecutionConstraintNames = [
      'maxTime',
      'maxTimeGlobal',
      'maxTokenStorageTime',
      'maxTokenStorageRounds',
      'maxMachineHops',
      'sameMachine',
    ];

    const remainingConstraints = hardConstraints.filter(
      (hardConstraint) => !processExecutionConstraintNames.includes(hardConstraint.name)
    );

    return Hceval.machineSatisfiesAllHardConstraints(remainingConstraints); // Step 2.4
  },

  /**
   * Function to find the optimal Machine in the MS
   * Skips steps 3.1, 3.2 and 3.4
   * @see  {@link https://gitlab.com/dBPMS-PROCEED/proceed/-/wikis/Engine/Universal/Process/Decision-Making#architecture-of-decider|Wiki Step 1 and 5}
   *
   * @param {Object} processInfo - see {@link findOptimalNextMachine()}
   * @param {flowNodeConstraints} - see {@link findOptimalNextMachine()}
   * @param {processConstraints} - see {@link findOptimalNextMachine()}
   * @returns {MachineResultObject} see {@link findOptimalNextMachine()}
   */
  async findOptimalExternalMachine(processInfo, flowNodeConstraints, processConstraints) {
    if (!processInfo) {
      throw new Error(
        "Missing input parameter for method 'findOptimalExternalMachine' in Decider Module"
      );
    }

    const machineResultObject = {
      engineList: [],
      prioritized: false,
    };

    const hardConstraints = constraintManager.concatAllConstraints(
      flowNodeConstraints.hardConstraints,
      processConstraints.hardConstraints
    );

    const softConstraints = constraintManager.concatAllConstraints(
      flowNodeConstraints.softConstraints,
      processConstraints.softConstraints
    );

    const processExecutionConstraintNames = [
      'maxTime',
      'maxTimeGlobal',
      'maxTokenStorageTime',
      'maxTokenStorageRounds',
      'maxMachineHops',
      'sameMachine',
    ];

    const remainingConstraints = hardConstraints.filter(
      (hardConstraint) => !processExecutionConstraintNames.includes(hardConstraint.name)
    );

    // Step 2.3 + 2.5
    const valuesList = await constraintManager.getExternalSoftConstraintValues(
      remainingConstraints,
      softConstraints
    );

    // Step 3
    const evaluatedMachines = Sceval.evaluateEveryMachine(softConstraints, valuesList);

    machineResultObject.engineList = evaluatedMachines;

    if (softConstraints.length > 0) {
      machineResultObject.prioritized = true;
    }

    return machineResultObject;
  },

  /**
   * Checks if instance/token has to be aborted
   * return with checkAbortResult-object indicating if instance or token has to be aborted, null if execution can continue
   * @param {Object} processInfo
   * @param {Object} token
   * @param {Array} flowNodeConstraints - hardConstraints of flowNode
   * @param {Array} processConstraints - hardConstraints of process
   * @returns {checkAbortResult} Returns wether the instance/token has to be aborted
   */
  async preCheckAbort(processInfo, token, flowNodeConstraints, processConstraints) {
    // use processInfo to get Env profile
    const checkAbortResult = {
      stopProcess: null,
      unfulfilledConstraints: [],
    };

    const process = await config.readConfig('process');

    const timeGlobal = Date.now() - token.globalStartTime;

    if (process.maxTimeProcessGlobal !== -1 && process.maxTimeProcessGlobal * 1000 < timeGlobal) {
      checkAbortResult.unfulfilledConstraints.push('maxTimeProcessGlobal');
    }

    if (
      process.maxTimeProcessLocal !== -1 &&
      process.maxTimeProcessLocal * 1000 < token.localExecutionTime
    ) {
      checkAbortResult.unfulfilledConstraints.push('maxTimeProcessLocal');
    }

    if (checkAbortResult.unfulfilledConstraints.length > 0) {
      checkAbortResult.stopProcess = 'instance';
      return checkAbortResult;
    }

    const filteredProcessConstraints = constraintManager.filterDuplicateProcessConstraints(
      flowNodeConstraints,
      processConstraints
    );

    const unfulfilledProcessConstraints = Hceval.evaluateExecutionConstraints(
      filteredProcessConstraints,
      {
        timeGlobal,
        time: token.localExecutionTime,
        machineHops: token.machineHops,
        storageTime: token.storageTime,
        storageRounds: token.storageRounds,
      }
    );

    if (unfulfilledProcessConstraints.length > 0) {
      checkAbortResult.unfulfilledConstraints = unfulfilledProcessConstraints.map(
        (constraint) => constraint.name
      );
      checkAbortResult.stopProcess = 'instance';
      return checkAbortResult;
    }

    if (process.maxTimeFlowNode !== -1 && process.maxTimeFlowNode * 1000 < token.flowNodeTime) {
      checkAbortResult.unfulfilledConstraints.push('maxTimeFlowNode');
      checkAbortResult.stopProcess = 'token';
      return checkAbortResult;
    }

    const unfulfilledFlowNodeConstraints = Hceval.evaluateExecutionConstraints(
      flowNodeConstraints,
      {
        time: token.flowNodeTime,
        machineHops: token.machineHops,
        storageTime: token.storageTime,
        storageRounds: token.storageRounds,
      }
    );

    if (unfulfilledFlowNodeConstraints.length > 0) {
      checkAbortResult.unfulfilledConstraints = unfulfilledFlowNodeConstraints.map(
        (constraint) => constraint.name
      );
      checkAbortResult.stopProcess = 'token';
      return checkAbortResult;
    }

    return checkAbortResult;
  },

  /**
   * Starts the endpoint for constraint evaluation
   */
  start() {
    routes();
  },
};
module.exports = decider;