/*
* Copyright (c) 2010-2016 Evolveum
*
* 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 com.evolveum.midpoint.wf.impl.activiti.dao;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.query.builder.QueryBuilder;
import com.evolveum.midpoint.schema.SearchResultList;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.result.OperationResultStatus;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.task.api.TaskManager;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.logging.LoggingUtils;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.wf.api.WorkflowManager;
import com.evolveum.midpoint.wf.impl.activiti.ActivitiEngine;
import com.evolveum.midpoint.wf.impl.processes.common.CommonProcessVariableNames;
import com.evolveum.midpoint.xml.ns._public.common.common_3.TaskType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.WfContextType;
import org.activiti.engine.HistoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.history.HistoricProcessInstance;
import org.activiti.engine.history.HistoricProcessInstanceQuery;
import org.activiti.engine.runtime.ProcessInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author mederly
*/
@Component
public class ProcessInstanceManager {
private static final transient Trace LOGGER = TraceManager.getTrace(ProcessInstanceManager.class);
@Autowired private ActivitiEngine activitiEngine;
@Autowired private TaskManager taskManager;
@Autowired private PrismContext prismContext;
private static final String DOT_INTERFACE = WorkflowManager.class.getName() + ".";
private static final String OPERATION_STOP_PROCESS_INSTANCE = DOT_INTERFACE + "stopProcessInstance";
private static final String OPERATION_DELETE_PROCESS_INSTANCE = DOT_INTERFACE + "deleteProcessInstance";
private static final String OPERATION_SYNCHRONIZE_WORKFLOW_REQUESTS = DOT_INTERFACE + "synchronizeWorkflowRequests";
public void stopProcessInstance(String instanceId, String username, OperationResult parentResult) {
OperationResult result = parentResult.createSubresult(OPERATION_STOP_PROCESS_INSTANCE);
result.addParam("instanceId", instanceId);
RuntimeService rs = activitiEngine.getRuntimeService();
try {
LOGGER.trace("Stopping process instance {} on the request of {}", instanceId, username);
String deletionMessage = "Process instance stopped on the request of " + username;
rs.setVariable(instanceId, CommonProcessVariableNames.VARIABLE_PROCESS_INSTANCE_IS_STOPPING, Boolean.TRUE);
rs.deleteProcessInstance(instanceId, deletionMessage);
} catch (RuntimeException e) {
result.recordFatalError("Process instance couldn't be stopped: " + e.getMessage(), e);
throw e;
} finally {
result.computeStatusIfUnknown();
}
}
private void deleteProcessInstance(String instanceId, OperationResult parentResult) {
OperationResult result = parentResult.createSubresult(OPERATION_DELETE_PROCESS_INSTANCE);
result.addParam("instanceId", instanceId);
HistoryService hs = activitiEngine.getHistoryService();
try {
hs.deleteHistoricProcessInstance(instanceId);
} catch (RuntimeException e) {
result.recordFatalError("Process instance couldn't be deleted: " + e.getMessage(), e);
throw e;
} finally {
result.computeStatusIfUnknown();
}
}
public void onTaskDelete(Task task, OperationResult result) {
try {
WfContextType wfc = task.getWorkflowContext();
if (wfc == null || wfc.getProcessInstanceId() == null) {
return;
}
String instanceId = wfc.getProcessInstanceId();
if (wfc.getEndTimestamp() == null) {
try {
stopProcessInstance(instanceId, "task delete action", result);
} catch (RuntimeException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't stop workflow process instance {} while processing task deletion event for task {}", e, instanceId, task);
}
}
deleteProcessInstance(instanceId, result);
} catch (RuntimeException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't process task deletion event for task {}", e, task);
}
}
class Statistics {
int processes = 0;
int processesWithNonExistingTaskOid = 0;
int processesWithNonWorkflowTaskOid = 0;
int processesWithWrongWorkflowTaskOid = 0;
int processesWithoutTaskOid = 0;
int processesRemoved = 0;
int wrongProcessesRemaining = 0;
int tasks = 0;
int tasksWithNonExistingPid = 0;
int tasksWithNonMidpointProcesses = 0;
int tasksWithWrongProcesses = 0;
int tasksRemoved = 0;
int wrongTasksRemaining = 0;
}
public void synchronizeWorkflowRequests(OperationResult parentResult) {
OperationResult result = parentResult.createSubresult(OPERATION_SYNCHRONIZE_WORKFLOW_REQUESTS);
try {
LOGGER.info("Starting synchronization of workflow requests between repository and Activiti");
final Set<String> activeProcessInstances = new HashSet<>();
final Map<String,String> activitiToMidpoint = getActivitiToMidpoint(activeProcessInstances, result);
final Map<String,String> midpointToActiviti = getMidpointToActiviti(result);
Statistics s = new Statistics();
doPhase1(activitiToMidpoint, midpointToActiviti, activeProcessInstances, s, result);
doPhase2(activitiToMidpoint, midpointToActiviti, s, result);
String message = s.processes + " processes found; out of these, removed " + s.processesRemoved + " ones; "
+ "remaining " + s.wrongProcessesRemaining + " wrong ones. " +
s.tasks + " tasks found; out of these, removed " + s.tasksRemoved + " ones; "
+ "remaining " + s.wrongTasksRemaining + " wrong ones.";
result.recordStatus(OperationResultStatus.SUCCESS, message);
} catch (RuntimeException|SchemaException e) {
result.recordFatalError("Workflow requests cannot be synchronized: " + e.getMessage());
} finally {
result.computeStatusIfUnknown();
LOGGER.info("Synchronization of workflow requests between repository and Activiti finished with the status of {}: {}",
result.getStatus(), result.getMessage());
}
}
protected void doPhase1(Map<String, String> activitiToMidpoint, Map<String, String> midpointToActiviti,
Set<String> activeProcessInstances, Statistics s, OperationResult result) {
s.processes = activitiToMidpoint.size();
final Iterator<Map.Entry<String,String>> iterator = activitiToMidpoint.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<String,String> entry = iterator.next();
final String pid = entry.getKey();
final String oid = entry.getValue();
if (oid != null) {
if (!midpointToActiviti.containsKey(oid)) {
LOGGER.warn("Activiti process {} points to non-existing task OID {} -- deleting it", pid, oid);
deleteProcessInstanceChecked(pid, activeProcessInstances, s, result);
iterator.remove();
s.processesWithNonExistingTaskOid++;
} else {
String pid2 = midpointToActiviti.get(oid);
if (pid2 == null) {
LOGGER.warn("Activiti process {} points to non-workflow task OID {} -- deleting it", pid, oid);
deleteProcessInstanceChecked(pid, activeProcessInstances, s, result);
iterator.remove();
s.processesWithNonWorkflowTaskOid++;
} else if (!pid2.equals(pid)) {
LOGGER.error(
"Activiti process {} points to task OID {} that points back to different process: {} -- please resolve manually",
pid, oid, pid2);
s.processesWithWrongWorkflowTaskOid++;
s.wrongProcessesRemaining++;
}
}
} else {
// probably no problem - this is an activiti process with no midPoint task attached
LOGGER.trace("Activiti process with no midPoint task attached: {}", pid);
s.processesWithoutTaskOid++;
}
}
LOGGER.info("Results of phase 1:\n"
+ "- processes with non-existing task OID: {}\n"
+ "- processes with non-workflow task OID: {}\n"
+ "- processes with wrong task OID (of such that points to other process instance): {}\n"
+ "- processes with no task OID: {}\n"
+ "- successfully deleted processes: {}", s.processesWithNonExistingTaskOid, s.processesWithNonWorkflowTaskOid,
s.processesWithWrongWorkflowTaskOid, s.processesWithoutTaskOid, s.processesRemoved);
}
protected void doPhase2(Map<String, String> activitiToMidpoint, Map<String, String> midpointToActiviti, Statistics s,
OperationResult result) {
s.tasks = midpointToActiviti.size();
final Iterator<Map.Entry<String,String>> iterator = midpointToActiviti.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<String,String> entry = iterator.next();
final String oid = entry.getKey();
final String pid = entry.getValue();
if (pid == null) {
// not a workflow-enabled task
continue;
}
if (!activitiToMidpoint.containsKey(pid)) {
LOGGER.warn("Task {} points to non-existing activiti process ID {} -- deleting it", oid, pid);
deleteTaskChecked(oid, s, result);
iterator.remove();
s.tasksWithNonExistingPid++;
continue;
}
String oid2 = activitiToMidpoint.get(pid);
if (oid2 == null) {
LOGGER.warn("Task {} points to non-midPoint activiti process ID {} -- deleting it", pid, oid);
deleteTaskChecked(oid, s, result);
iterator.remove();
s.tasksWithNonMidpointProcesses++;
continue;
}
if (!oid2.equals(oid)) {
LOGGER.warn("Task {} points to activiti process ID {} that points back to different task -- please resolve manually", pid, oid);
s.tasksWithWrongProcesses++;
s.wrongTasksRemaining++;
}
}
LOGGER.info("Results of phase 2:\n"
+ "- tasks with non-existing process ID: {}\n"
+ "- tasks with non-midPoint process ID: {}\n"
+ "- tasks with wrong process ID (such that points to other task): {}\n"
+ "- successfully deleted tasks: {}",
s.tasksWithNonExistingPid, s.tasksWithNonMidpointProcesses, s.tasksWithWrongProcesses,
s.tasksRemoved);
}
private void deleteProcessInstanceChecked(String pid, Set<String> activeProcessInstances, Statistics s, OperationResult result) {
try {
if (activeProcessInstances.contains(pid)) {
try {
stopProcessInstance(pid, "workflow requests synchronization process", result);
} catch (RuntimeException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't stop process instance {}", e, pid);
}
}
deleteProcessInstance(pid, result);
s.processesRemoved++;
} catch (RuntimeException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't remove process instance {}", e, pid);
s.wrongProcessesRemaining++;
}
}
private void deleteTaskChecked(String oid, Statistics s, OperationResult result) {
try {
taskManager.deleteTask(oid, result);
s.tasksRemoved++;
} catch (ObjectNotFoundException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't remove task {} as it seems to be no longer existing", e, oid);
} catch (RuntimeException|SchemaException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't remove task {}", e, oid);
s.wrongTasksRemaining++;
}
}
private Map<String, String> getMidpointToActiviti(OperationResult result) throws SchemaException {
final Map<String,String> rv = new HashMap<>();
final List<PrismObject<TaskType>> tasks = taskManager.searchObjects(TaskType.class, null, null, result);
int tasksWithProcessId = 0;
for (PrismObject<TaskType> taskObject : tasks) {
final TaskType task = taskObject.asObjectable();
final WfContextType wfc = task.getWorkflowContext();
final String pid = wfc != null ? wfc.getProcessInstanceId() : null;
rv.put(task.getOid(), pid);
if (pid != null) {
tasksWithProcessId++;
}
}
LOGGER.info("Found {} tasks; among these, {} have a pointer to process instance id", rv.size(), tasksWithProcessId);
return rv;
}
private Map<String, String> getActivitiToMidpoint(Set<String> activeProcessInstances, OperationResult result) {
Map<String,String> rv = new HashMap<>();
int processWithoutTaskOidCount = 0;
HistoricProcessInstanceQuery query = activitiEngine.getHistoryService().createHistoricProcessInstanceQuery()
.includeProcessVariables()
.excludeSubprocesses(true);
List<HistoricProcessInstance> processes = query.list();
for (HistoricProcessInstance process : processes) {
String taskOid = (String) process.getProcessVariables().get(CommonProcessVariableNames.VARIABLE_MIDPOINT_TASK_OID);
rv.put(process.getId(), taskOid);
if (taskOid == null) {
processWithoutTaskOidCount++;
}
if (process.getEndTime() == null) {
activeProcessInstances.add(process.getId());
}
}
LOGGER.info("Found {} processes; among these, {} have no task OID. Active processes: {}",
rv.size(), processWithoutTaskOidCount, activeProcessInstances.size());
return rv;
}
public void cleanupActivitiProcesses(OperationResult result) throws SchemaException {
RuntimeService runtimeService = activitiEngine.getRuntimeService();
TaskService taskService = activitiEngine.getTaskService();
LOGGER.info("Starting cleanup of Activiti processes");
Collection<String> processInstancesToKeep = getProcessInstancesToKeep(result);
LOGGER.info("Process instances to keep: {}", processInstancesToKeep);
List<ProcessInstance> instances = runtimeService.createProcessInstanceQuery().list();
LOGGER.info("Existing process instances in Activiti: {}", instances.size());
int ok = 0, fail = 0;
for (ProcessInstance instance : instances) {
String instanceId = instance.getId();
if (processInstancesToKeep.contains(instanceId)) {
continue;
}
LOGGER.debug("Deleting process instance {}", instance);
try {
runtimeService.setVariable(instanceId, CommonProcessVariableNames.VARIABLE_PROCESS_INSTANCE_IS_STOPPING, Boolean.TRUE);
runtimeService.deleteProcessInstance(instanceId, "Deleted as part of activiti processes cleanup");
ok++;
} catch (Throwable t) {
LOGGER.info("Couldn't delete process instance {}, retrying with explicit deletion of some variables for its tasks", instanceId);
List<org.activiti.engine.task.Task> tasks = taskService.createTaskQuery()
.processInstanceId(instanceId).list();
LOGGER.debug("Tasks: {}", tasks);
for (org.activiti.engine.task.Task task : tasks) {
taskService.removeVariables(task.getId(), Arrays.asList("approvalSchema", "level"));
}
try {
runtimeService.deleteProcessInstance(instanceId, "Deleted as part of activiti processes cleanup");
ok++;
} catch (Throwable t2) {
result.createSubresult(ProcessInstanceManager.class.getName() + ".cleanupActivitiProcess")
.recordPartialError("Couldn't delete Activiti process instance " + instanceId + ": " + t2.getMessage(),
t2);
fail++;
}
}
}
String message = "Successfully deleted "+ok+" instances; failed "+fail+" times";
LOGGER.info(message);
result.recordStatus(fail > 0 ? OperationResultStatus.PARTIAL_ERROR : OperationResultStatus.SUCCESS, message);
}
private Set<String> getProcessInstancesToKeep(OperationResult result) throws SchemaException {
ObjectQuery query = QueryBuilder.queryFor(TaskType.class, prismContext)
.not().item(TaskType.F_WORKFLOW_CONTEXT, WfContextType.F_PROCESS_INSTANCE_ID).isNull()
.build();
SearchResultList<PrismObject<TaskType>> tasks = taskManager.searchObjects(TaskType.class, query, null, result);
return tasks.stream()
.map(t -> t.asObjectable().getWorkflowContext().getProcessInstanceId())
.collect(Collectors.toSet());
}
}