/* * Copyright 2016 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jbpm.runtime.manager.impl.migration; import org.drools.core.command.SingleSessionCommandService; import org.drools.core.command.impl.CommandBasedStatefulKnowledgeSession; import org.drools.core.common.InternalKnowledgeRuntime; import org.drools.persistence.api.SessionNotFoundException; import org.drools.persistence.api.TransactionManager; import org.drools.persistence.api.TransactionManagerFactory; import org.jbpm.process.audit.JPAAuditLogService; import org.jbpm.process.audit.ProcessInstanceLog; import org.jbpm.runtime.manager.impl.jpa.EntityManagerFactoryManager; import org.jbpm.runtime.manager.impl.migration.MigrationEntry.Type; import org.jbpm.workflow.core.NodeContainer; import org.jbpm.workflow.core.impl.NodeImpl; import org.jbpm.workflow.core.node.HumanTaskNode; import org.jbpm.workflow.instance.NodeInstanceContainer; import org.jbpm.workflow.instance.impl.NodeInstanceImpl; import org.jbpm.workflow.instance.impl.WorkflowProcessInstanceImpl; import org.jbpm.workflow.instance.node.HumanTaskNodeInstance; import org.kie.api.definition.process.Node; import org.kie.api.definition.process.WorkflowProcess; import org.kie.api.runtime.KieRuntime; import org.kie.api.runtime.KieSession; import org.kie.api.runtime.process.NodeInstance; import org.kie.api.runtime.process.ProcessInstance; import org.kie.internal.persistence.jpa.JPAKnowledgeService; import org.kie.internal.runtime.manager.InternalRuntimeManager; import org.kie.internal.runtime.manager.RuntimeManagerRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Query; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * MigrationManager is responsible for updating all required components during process instance migration. * Each process instance should be have dedicated instance of the manager to allow simple execution model. * Each manager maintains MigrationReport that is constantly updated when migration is running. * * It comes with following migration entries (as part of the report) * <ul> * <li>INFO - written mostly for information about given migration step and its result</li> * <li>WARN - not recommended operation performed though did not stop the migration</li> * <li>ERROR - terminates the migration and restores to last state - before migration</li> * </ul> * There could be at most single ERROR type of entry as first one that occurred terminates the migration. * * Migration is composed of two steps * <ul> * <li>validation - various checks to ensure migration can be performed to limit number of failed migrations</li> * <li>migration - actual migration that changes state of the process instance and its index data - history logs</li> * <ul> * * Migration can either be performed with or without node instance mapping. Node instance mapping allows to map nodes * only of the same type as it simply changes node reference of the node and does not replace the node instance * (by canceling current node and triggering new one). */ public class MigrationManager { private static final Logger logger = LoggerFactory.getLogger(MigrationManager.class); private MigrationReport report; private MigrationSpec migrationSpec; /** * Creates new instance of MigrationManager with given migration specification. * Migration specification will be validated upon call to {@link #migrate()} method * @param migrationSpec definition of what needs to be migrated */ public MigrationManager(MigrationSpec migrationSpec) { this.report = new MigrationReport(migrationSpec); this.migrationSpec = migrationSpec; } /** * Performs migration without node instance mapping * @return returns migration report describing complete migration process. */ public MigrationReport migrate() { return migrate(null); } /** * Performs migration with node mapping (if non null). * @param nodeMapping node instance mapping that is composed of unique ids of source node mapped to target node * @return returns migration report describing complete migration process. */ public MigrationReport migrate(Map<String, String> nodeMapping) { validate(); KieSession current = null; KieSession tobe = null; TransactionManager txm = null; boolean transactionOwner = false; try { InternalRuntimeManager currentManager = (InternalRuntimeManager) RuntimeManagerRegistry.get().getManager(migrationSpec.getDeploymentId()); InternalRuntimeManager toBeManager = (InternalRuntimeManager) RuntimeManagerRegistry.get().getManager(migrationSpec.getToDeploymentId()); // start transaction to secure consistency of the migration txm = TransactionManagerFactory.get().newTransactionManager(currentManager.getEnvironment().getEnvironment()); transactionOwner = txm.begin(); org.kie.api.definition.process.Process toBeProcess = toBeManager.getEnvironment().getKieBase().getProcess(migrationSpec.getToProcessId()); String auditPu = currentManager.getDeploymentDescriptor().getAuditPersistenceUnit(); EntityManagerFactory emf = EntityManagerFactoryManager.get().getOrCreate(auditPu); EntityManager em = emf.createEntityManager(); try { // update variable instance log information with new deployment id and process id Query varLogQuery = em.createQuery("update VariableInstanceLog set externalId = :depId, processId = :procId where processInstanceId = :procInstanceId"); varLogQuery .setParameter("depId", migrationSpec.getToDeploymentId()) .setParameter("procId", migrationSpec.getToProcessId()) .setParameter("procInstanceId", migrationSpec.getProcessInstanceId()); int varsUpdated = varLogQuery.executeUpdate(); report.addEntry(Type.INFO, "Variable instances updated = " + varsUpdated + " for process instance id " + migrationSpec.getProcessInstanceId()); // update node instance log information with new deployment id and process id Query nodeLogQuery = em.createQuery("update NodeInstanceLog set externalId = :depId, processId = :procId where processInstanceId = :procInstanceId"); nodeLogQuery .setParameter("depId", migrationSpec.getToDeploymentId()) .setParameter("procId", migrationSpec.getToProcessId()) .setParameter("procInstanceId", migrationSpec.getProcessInstanceId()); int nodesUpdated = nodeLogQuery.executeUpdate(); report.addEntry(Type.INFO, "Node instances updated = " + nodesUpdated + " for process instance id " + migrationSpec.getProcessInstanceId()); // update process instance log with new deployment and process id Query pInstanceLogQuery = em.createQuery("update ProcessInstanceLog set externalId = :depId, processId = :procId, processName = :procName, processVersion= :procVersion where processInstanceId = :procInstanceId"); pInstanceLogQuery .setParameter("depId", migrationSpec.getToDeploymentId()) .setParameter("procId", migrationSpec.getToProcessId()) .setParameter("procName", toBeProcess.getName()) .setParameter("procVersion", toBeProcess.getVersion()) .setParameter("procInstanceId", migrationSpec.getProcessInstanceId()); int pInstancesUpdated = pInstanceLogQuery.executeUpdate(); report.addEntry(Type.INFO, "Process instances updated = " + pInstancesUpdated + " for process instance id " + migrationSpec.getProcessInstanceId()); try { // update task audit instance log with new deployment and process id Query taskVarLogQuery = em.createQuery("update TaskVariableImpl set processId = :procId where processInstanceId = :procInstanceId"); taskVarLogQuery .setParameter("procId", migrationSpec.getToProcessId()) .setParameter("procInstanceId", migrationSpec.getProcessInstanceId()); int taskVarUpdated = taskVarLogQuery.executeUpdate(); report.addEntry(Type.INFO, "Task variables updated = " + taskVarUpdated + " for process instance id " + migrationSpec.getProcessInstanceId()); } catch (Throwable e) { logger.warn("Unexpected error during migration", e); report.addEntry(Type.WARN, "Cannot update task variables (added in version 6.3) due to " + e.getMessage()); } // update task audit instance log with new deployment and process id Query auditTaskLogQuery = em.createQuery("update AuditTaskImpl set deploymentId = :depId, processId = :procId where processInstanceId = :procInstanceId"); auditTaskLogQuery .setParameter("depId", migrationSpec.getToDeploymentId()) .setParameter("procId", migrationSpec.getToProcessId()) .setParameter("procInstanceId", migrationSpec.getProcessInstanceId()); int auditTaskUpdated = auditTaskLogQuery.executeUpdate(); report.addEntry(Type.INFO, "Task audit updated = " + auditTaskUpdated + " for process instance id " + migrationSpec.getProcessInstanceId()); // update task instance log with new deployment and process id Query taskLogQuery = em.createQuery("update TaskImpl set deploymentId = :depId, processId = :procId where processInstanceId = :procInstanceId"); taskLogQuery .setParameter("depId", migrationSpec.getToDeploymentId()) .setParameter("procId", migrationSpec.getToProcessId()) .setParameter("procInstanceId", migrationSpec.getProcessInstanceId()); int taskUpdated = taskLogQuery.executeUpdate(); report.addEntry(Type.INFO, "Tasks updated = " + taskUpdated + " for process instance id " + migrationSpec.getProcessInstanceId()); try { // update context mapping info with new deployment Query contextInfoQuery = em.createQuery("update ContextMappingInfo set ownerId = :depId where contextId = :procInstanceId"); contextInfoQuery .setParameter("depId", migrationSpec.getToDeploymentId()) .setParameter("procInstanceId", migrationSpec.getProcessInstanceId().toString()); int contextInfoUpdated = contextInfoQuery.executeUpdate(); report.addEntry(Type.INFO, "Context info updated = " + contextInfoUpdated+ " for process instance id " + migrationSpec.getProcessInstanceId()); } catch (Throwable e) { logger.warn("Unexpected error during migration", e); report.addEntry(Type.WARN, "Cannot update context mapping owner (added in version 6.2) due to " + e.getMessage()); } current = JPAKnowledgeService.newStatefulKnowledgeSession(currentManager.getEnvironment().getKieBase(), null, currentManager.getEnvironment().getEnvironment()); tobe = JPAKnowledgeService.newStatefulKnowledgeSession(toBeManager.getEnvironment().getKieBase(), null, toBeManager.getEnvironment().getEnvironment()); upgradeProcessInstance(current, tobe, migrationSpec.getProcessInstanceId(), migrationSpec.getToProcessId(), nodeMapping, em, toBeManager.getIdentifier()); em.flush(); } finally { em.clear(); em.close(); } txm.commit(transactionOwner); report.addEntry(Type.INFO, "Migration of process instance (" + migrationSpec.getProcessInstanceId() + ") completed successfully to process " + migrationSpec.getToProcessId()); report.setSuccessful(true); report.setEndDate(new Date()); } catch (Throwable e) { txm.rollback(transactionOwner); logger.error("Unexpected error during migration", e); report.addEntry(Type.ERROR, "Migration of process instance (" + migrationSpec.getProcessInstanceId() + ") failed due to " + e.getMessage()); } finally { if (current != null) { try { current.destroy(); } catch (SessionNotFoundException e) { // in case of rollback session might not exist } } if (tobe != null) { try { tobe.destroy(); } catch (SessionNotFoundException e) { // in case of rollback session might not exist } } } return report; } private void validate() { if (migrationSpec == null) { report.addEntry(Type.ERROR, "no process data given for migration"); return; } // source (active) process instance information if (isEmpty(migrationSpec.getDeploymentId())) { report.addEntry(Type.ERROR, "No deployment id set"); } if (migrationSpec.getProcessInstanceId() == null) { report.addEntry(Type.ERROR, "No process instance id set"); } // target process information if (isEmpty(migrationSpec.getToDeploymentId())) { report.addEntry(Type.ERROR, "No target deployment id set"); } if (isEmpty(migrationSpec.getToProcessId())) { report.addEntry(Type.ERROR, "No target process id set"); } // verify if given runtime manager exists - registered under source deployment id if (!RuntimeManagerRegistry.get().isRegistered(migrationSpec.getDeploymentId())) { report.addEntry(Type.ERROR, "No deployment found for " + migrationSpec.getDeploymentId()); } // verify if given runtime manager exists - registered under target deployment id if (!RuntimeManagerRegistry.get().isRegistered(migrationSpec.getToDeploymentId())) { report.addEntry(Type.ERROR, "No target deployment found for " + migrationSpec.getToDeploymentId()); } // verify if given target process id exists in target runtime manager InternalRuntimeManager manager = (InternalRuntimeManager) RuntimeManagerRegistry.get().getManager(migrationSpec.getToDeploymentId()); if (manager.getEnvironment().getKieBase().getProcess(migrationSpec.getToProcessId()) == null) { report.addEntry(Type.ERROR, "No process found for " + migrationSpec.getToProcessId() + " in deployment " + migrationSpec.getToDeploymentId()); } String auditPu = manager.getDeploymentDescriptor().getAuditPersistenceUnit(); EntityManagerFactory emf = EntityManagerFactoryManager.get().getOrCreate(auditPu); JPAAuditLogService auditService = new JPAAuditLogService(emf); try { ProcessInstanceLog log = auditService.findProcessInstance(migrationSpec.getProcessInstanceId()); if (log == null || log.getStatus() != ProcessInstance.STATE_ACTIVE) { report.addEntry(Type.ERROR, "No process instance found or it is not active (id " + migrationSpec.getProcessInstanceId() + " in status " + (log == null?"-1":log.getStatus())); } } finally { auditService.dispose(); } } private void upgradeProcessInstance(KieRuntime oldkruntime, KieRuntime kruntime, long processInstanceId, String processId, Map<String, String> nodeMapping, EntityManager em, String deploymentId) { if (nodeMapping == null) { nodeMapping = new HashMap<String, String>(); } WorkflowProcessInstanceImpl processInstance = (WorkflowProcessInstanceImpl) oldkruntime.getProcessInstance(processInstanceId); if (processInstance == null) { report.addEntry(Type.ERROR, "Could not find process instance " + processInstanceId); } if (processId == null) { report.addEntry(Type.ERROR, "Null process id"); } WorkflowProcess process = (WorkflowProcess) kruntime.getKieBase().getProcess(processId); if (process == null) { report.addEntry(Type.ERROR, "Could not find process " + processId); } if (processInstance.getProcessId().equals(processId)) { report.addEntry(Type.WARN, "Source and target process id is exactly the same (" + processId + ") it's recommended to use unique process ids"); } synchronized (processInstance) { org.kie.api.definition.process.Process oldProcess = processInstance.getProcess(); processInstance.disconnect(); processInstance.setProcess(oldProcess); updateNodeInstances(processInstance, nodeMapping, (NodeContainer) process, em); processInstance.setKnowledgeRuntime((InternalKnowledgeRuntime) extractIfNeeded(kruntime)); processInstance.setDeploymentId(deploymentId); processInstance.setProcess(process); processInstance.reconnect(); } } @SuppressWarnings("unchecked") private void updateNodeInstances(NodeInstanceContainer nodeInstanceContainer, Map<String, String> nodeMapping, NodeContainer nodeContainer, EntityManager em) { for (NodeInstance nodeInstance: nodeInstanceContainer.getNodeInstances()) { Long upgradedNodeId = null; String oldNodeId = (String) ((NodeImpl) ((org.jbpm.workflow.instance.NodeInstance) nodeInstance).getNode()).getMetaData().get("UniqueId"); String newNodeId = nodeMapping.get(oldNodeId); if (newNodeId == null) { newNodeId = oldNodeId; } Node upgradedNode = findNodeByUniqueId(newNodeId, nodeContainer); if (upgradedNode == null) { try { upgradedNodeId = Long.parseLong(newNodeId); } catch (NumberFormatException e) { continue; } } else { upgradedNodeId = upgradedNode.getId(); } ((NodeInstanceImpl) nodeInstance).setNodeId(upgradedNodeId); if (upgradedNode != null) { // update log information for new node information Query nodeInstanceIdQuery = em.createQuery("select nodeInstanceId from NodeInstanceLog nil" + " where nil.nodeId = :oldNodeId and processInstanceId = :processInstanceId " + " GROUP BY nil.nodeInstanceId" + " HAVING sum(nil.type) = 0"); nodeInstanceIdQuery .setParameter("oldNodeId", oldNodeId) .setParameter("processInstanceId", nodeInstance.getProcessInstance().getId()); List<Long> nodeInstanceIds = nodeInstanceIdQuery.getResultList(); report.addEntry(Type.INFO, "Mapping: Node instance logs to be updated = " + nodeInstanceIds); Query nodeLogQuery = em.createQuery("update NodeInstanceLog set nodeId = :nodeId, nodeName = :nodeName, nodeType = :nodeType " + "where nodeInstanceId in (:ids) and processInstanceId = :processInstanceId"); nodeLogQuery .setParameter("nodeId", (String) upgradedNode.getMetaData().get("UniqueId")) .setParameter("nodeName", upgradedNode.getName()) .setParameter("nodeType", upgradedNode.getClass().getSimpleName()) .setParameter("ids", nodeInstanceIds) .setParameter("processInstanceId", nodeInstance.getProcessInstance().getId()); int nodesUpdated = nodeLogQuery.executeUpdate(); report.addEntry(Type.INFO, "Mapping: Node instance logs updated = " + nodesUpdated + " for node instance id " + nodeInstance.getId()); if (upgradedNode instanceof HumanTaskNode && nodeInstance instanceof HumanTaskNodeInstance) { Long taskId = (Long) em.createQuery("select id from TaskImpl where workItemId = :workItemId") .setParameter("workItemId", ((HumanTaskNodeInstance) nodeInstance).getWorkItemId()) .getSingleResult(); String name = ((HumanTaskNode) upgradedNode).getName(); String description = (String)((HumanTaskNode) upgradedNode).getWork().getParameter("Description"); // update task audit instance log with new deployment and process id Query auditTaskLogQuery = em.createQuery("update AuditTaskImpl set name = :name, description = :description where taskId = :taskId"); auditTaskLogQuery .setParameter("name", name) .setParameter("description", description) .setParameter("taskId", taskId); int auditTaskUpdated = auditTaskLogQuery.executeUpdate(); report.addEntry(Type.INFO, "Mapping: Task audit updated = " + auditTaskUpdated + " for task id " + taskId); // update task instance log with new deployment and process id Query taskLogQuery = em.createQuery("update TaskImpl set name = :name, description = :description where id = :taskId"); taskLogQuery .setParameter("name", name) .setParameter("description", description) .setParameter("taskId", taskId); int taskUpdated = taskLogQuery.executeUpdate(); report.addEntry(Type.INFO, "Mapping: Task updated = " + taskUpdated + " for task id " + taskId); } } if (nodeInstance instanceof NodeInstanceContainer) { updateNodeInstances((NodeInstanceContainer) nodeInstance, nodeMapping, nodeContainer, em); } } } private Node findNodeByUniqueId(String uniqueId, NodeContainer nodeContainer) { Node result = null; for (Node node : nodeContainer.getNodes()) { if (uniqueId.equals(node.getMetaData().get("UniqueId"))) { return node; } if (node instanceof NodeContainer) { result = findNodeByUniqueId(uniqueId, (NodeContainer) node); if (result != null) { return result; } } } return result; } private KieRuntime extractIfNeeded(KieRuntime ksession) { if (ksession instanceof CommandBasedStatefulKnowledgeSession) { return ((SingleSessionCommandService)((CommandBasedStatefulKnowledgeSession) ksession).getRunner()).getKieSession(); } return ksession; } private boolean isEmpty(String value) { if (value == null || value.isEmpty()) { return true; } return false; } }