/*
* Copyright (c) NASK, NCSC
*
* This file is part of HoneySpider Network 2.1.
*
* This is a free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package pl.nask.hsn2.workflow.engine;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.activiti.engine.impl.pvm.PvmExecution;
import org.activiti.engine.impl.pvm.PvmProcessDefinition;
import org.activiti.engine.impl.pvm.PvmProcessInstance;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.nask.hsn2.activiti.ExtendedExecutionImpl;
import pl.nask.hsn2.activiti.behavior.HSNBehavior;
import pl.nask.hsn2.bus.api.BusManager;
import pl.nask.hsn2.bus.connector.objectstore.ObjectStoreConnectorException;
import pl.nask.hsn2.bus.operations.JobStatus;
import pl.nask.hsn2.bus.operations.ObjectData;
import pl.nask.hsn2.bus.operations.TaskErrorReasonType;
import pl.nask.hsn2.framework.bus.FrameworkBus;
import pl.nask.hsn2.framework.suppressor.JobSuppressorHelper;
import pl.nask.hsn2.framework.workflow.engine.ProcessBasedWorkflowDescriptor;
import pl.nask.hsn2.framework.workflow.job.DefaultTasksStatistics;
import pl.nask.hsn2.framework.workflow.job.WorkflowJob;
import pl.nask.hsn2.framework.workflow.job.WorkflowJobInfo;
public final class ActivitiJob implements WorkflowJob, WorkflowJobInfo {
private static final Logger LOGGER = LoggerFactory.getLogger(ActivitiJob.class);
private final ProcessBasedWorkflowDescriptor<PvmProcessDefinition> workflowDefinitionDescriptor;
private final PvmProcessDefinition processDefinition;
private PvmProcessInstance processInstance;
private long jobId;
private TaskErrorReasonType failureReason;
private volatile boolean running = false;
private volatile boolean cancelled = false;
private Map<String, Integer> errorMessages = new ConcurrentHashMap<>();
private Map<String, Properties> userConfig;
private long startTime = 0;
private long endTime = 0;
private String lastActiveStepName = "initialization";
private long objectDataId;
private DefaultTasksStatistics stats = new DefaultTasksStatistics();
public ActivitiJob(PvmProcessDefinition processDefinition,
ProcessBasedWorkflowDescriptor<PvmProcessDefinition> workflowDefinitionDescriptorImpl,
Map<String, Properties> workflowConfig) {
this.processDefinition = processDefinition;
this.workflowDefinitionDescriptor = workflowDefinitionDescriptorImpl;
this.userConfig = workflowConfig;
}
@Override
public synchronized void start(long jobId, JobSuppressorHelper jobSuppressorHelper) {
if (processInstance != null) {
throw new IllegalStateException("Job already started");
} else {
processInstance = processDefinition.createProcessInstance();
startTime = System.currentTimeMillis();
this.jobId = jobId;
try{
this.objectDataId = createInitialObject(jobId);
ExecutionWrapper utils = new ExecutionWrapper(processInstance);
utils.initProcessState(jobId, objectDataId, userConfig, workflowDefinitionDescriptor, stats, jobSuppressorHelper);
processInstance.start();
this.running = true;
((FrameworkBus)BusManager.getBus()).jobStarted(jobId);
updateJobDetails();
LOGGER.info("Job started (jobId={}, userConfig={}, workflowDefinition={}, initialObjectId={}", new Object[] {jobId, userConfig, workflowDefinitionDescriptor, objectDataId});
}
catch(ObjectStoreConnectorException e){
failureReason = TaskErrorReasonType.OBJ_STORE;
addErrorMessage("Problem with connection to objectStore: " + e.getMessage());
endTime = System.currentTimeMillis();
LOGGER.error(e.getMessage(), e);
}
}
}
private long createInitialObject(long jobId) throws ObjectStoreConnectorException {
return ((FrameworkBus)BusManager.getBus()).getObjectStoreConnector().sendObjectStoreData(jobId, new ObjectData());
}
@Override
public boolean isEnded() {
return !running;
}
@Override
public JobStatus getStatus() {
if (isFailed()) {
return JobStatus.FAILED;
} else if (isCancelled()) {
return JobStatus.CANCELLED;
} else if (isEnded()) {
return JobStatus.COMPLETED;
} else {
return JobStatus.PROCESSING;
}
}
private boolean isCancelled() {
return cancelled;
}
private boolean isFailed() {
return failureReason != null;
}
@Override
public synchronized void markTaskAsAccepted(int requestId) {
if (running) {
ExecutionWrapper execution = findExecutionForTaskId(requestId);
if (execution == null) {
LOGGER.debug("Cannot find execution for taskId={}. The task may be already completed", requestId);
} else {
// don't care, if the task was accepted previously.
execution.markTaskAsAccepted();
}
} else {
// Send reminder that job is already finished.
((FrameworkBus) BusManager.getBus()).jobFinishedReminder(jobId, getStatus(), requestId);
LOGGER.debug("Job (id={}) is not running. Can not mark task (id={}) as accepted", jobId, requestId);
}
}
private ExecutionWrapper findExecutionForTaskId(int requestId) {
long searchStartTime = System.currentTimeMillis();
ExtendedExecutionImpl extendedProcessInstance = (ExtendedExecutionImpl) processInstance;
PvmExecution execution = extendedProcessInstance.findExecuionWithTaskId(requestId);
if (execution != null) {
ExecutionWrapper wrapper = new ExecutionWrapper(execution);
// verify taskId
Integer taskId = wrapper.getTaskId();
if (taskId != null && taskId.equals(requestId)) {
LOGGER.debug("Execution (jobId={}, taskId={}) found in {} ms", new Object[] {jobId, requestId, System.currentTimeMillis() - searchStartTime});
return wrapper;
}
}
LOGGER.debug("Execution (jobId={}, taskId={}) not found after {} ms", new Object[] {jobId, requestId, System.currentTimeMillis() - searchStartTime});
return null;
}
@Override
public synchronized void markTaskAsCompleted(int requestId, Set<Long> newObjects) {
if (running) {
ExecutionWrapper execution = findExecutionForTaskId(requestId);
if (execution == null) {
LOGGER.warn("Execution for taskId={} cannot be found. The task may be already completed.", requestId);
} else {
if (newObjects != null) {
for (Long objectId: newObjects) {
execution.subprocess(workflowDefinitionDescriptor, objectId);
}
resumeAndUpdateJobDetails();
}
try {
execution.signal("completeTask", requestId);
updateJobDetails();
} catch (Exception e) {
LOGGER.error("Error processing job", e);
markTaskAsFailed(requestId, TaskErrorReasonType.DEFUNCT, e.getMessage());
}
}
if (processInstance.isEnded()) {
finishJob();
}
} else {
// Send reminder that job is already finished.
((FrameworkBus) BusManager.getBus()).jobFinishedReminder(jobId, getStatus(), requestId);
LOGGER.debug("Job (id={}) is not running. Can not mark task (id={}) as completed", jobId, requestId);
}
}
public long getId() {
return jobId;
}
@Override
public synchronized void markTaskAsFailed(int requestId, TaskErrorReasonType reason, String description) {
if (running) {
ExecutionWrapper exec = findExecutionForTaskId(requestId);
try {
addErrorMessage(description);
exec.signal("taskFailed", new Object[] {requestId, reason, description});
updateJobDetails();
} catch(Exception e) {
this.failureReason = reason;
this.lastActiveStepName = getActiveStepName();
LOGGER.info("Job failed (jobId={}, taskId={}, stepName={}, reason={}, errorMsg={})", new Object[] {jobId, requestId, this.lastActiveStepName, reason, description});
processInstance.deleteCascade(description);
finishJob();
}
} else {
LOGGER.debug("Job (id={}) is not running (already failed). Can not mark task (id={}) as failed. Ignoring new failure reason {} ({})", new Object[] {jobId, requestId, reason, description});
// Send reminder that job is already finished.
((FrameworkBus) BusManager.getBus()).jobFinishedReminder(jobId, getStatus(), requestId);
LOGGER.debug("Job (id={}) is not running. Can not mark task (id={}) as completed", jobId, requestId);
}
}
private void addErrorMessage(String msg) {
Integer i = errorMessages.get(msg);
if (i == null) {
i = 1;
} else {
i++;
}
errorMessages.put(msg, i);
}
private void finishJob(){
this.endTime = System.currentTimeMillis();
this.running = false;
updateJobDetails();
ExecutionWrapper utils = new ExecutionWrapper(processInstance);
utils.getProcessContext().removeJobSuppressorHelper();
try{
((FrameworkBus)BusManager.getBus()).getObjectStoreConnector().sendJobFinished(jobId, getStatus());
}
catch (ObjectStoreConnectorException e) {
LOGGER.error("Error when sending JobFinished to store!",e);
}
LOGGER.info("Job {} {} time: {}",new Object[]{jobId,getStatus(),endTime - startTime});
}
@Override
public synchronized void resume() {
if (running) {
resumeAndUpdateJobDetails();
} else {
LOGGER.debug("Job (id={}) is not running. Can not resume it's processes", jobId);
}
}
private void resumeAndUpdateJobDetails() {
ExecutionWrapper wrapper = new ExecutionWrapper(processInstance);
try {
wrapper.signal("resume");
updateJobDetails();
} catch (Exception e) {
LOGGER.error("Error processing job", e);
markTaskAsFailed(wrapper.getTaskId() == null ? -1 : wrapper.getTaskId(), TaskErrorReasonType.DEFUNCT, e.getMessage());
}
}
private void updateJobDetails(){
ActivityImpl activity = (ActivityImpl) processInstance.getActivity();
if (activity != null) {
HSNBehavior behavior = (HSNBehavior) activity.getActivityBehavior();
lastActiveStepName = behavior.getStepName();
}
running = !processInstance.isEnded();
}
@Override
public synchronized String getActiveStepName() {
return lastActiveStepName;
}
@Override
public boolean isErrorMessagesReceived(){
return !errorMessages.isEmpty();
}
@Override
public String getErrorMessage() {
StringBuilder sb = new StringBuilder();
for (Entry<String, Integer> errMsgEntry : errorMessages.entrySet()) {
if (sb.length() != 0) {
sb.append("; ");
}
sb.append(errMsgEntry.getKey()).append(" (").append(errMsgEntry.getValue()).append(")");
}
return sb.toString();
}
@Override
public Map<String, Properties> getUserConfig() {
return userConfig;
}
@Override
public long getStartTime() {
return startTime;
}
@Override
public long getEndTime() {
return endTime;
}
@Override
public synchronized int getActiveSubtasksCount() {
ExecutionWrapper utils = new ExecutionWrapper(processInstance);
return utils.countActiveSubprocesses();
}
@Override
public String getWorkflowRevision() {
return workflowDefinitionDescriptor.getId();
}
@Override
public String getWorkflowName() {
return workflowDefinitionDescriptor.getName();
}
@Override
public DefaultTasksStatistics getTasksStatistics() {
return stats;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[")
.append("jobID:").append(jobId)
.append(", status:").append(getStatus().name().toString())
.append(", workflow name:").append(workflowDefinitionDescriptor.getName())
.append(", last step:").append(lastActiveStepName)
.append(", active tasks:").append(getActiveSubtasksCount())
.append("]");
return sb.toString();
}
@Override
public synchronized void cancel(){
if (running){
cancelled = true;
this.lastActiveStepName = getActiveStepName();
processInstance.deleteCascade("Cancelled by user.");
finishJob();
}
}
}