/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.component.workflow.execution.internal;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledFuture;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.framework.BundleContext;
import de.rcenvironment.core.communication.api.CommunicationService;
import de.rcenvironment.core.communication.api.PlatformService;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.common.LogicalNodeId;
import de.rcenvironment.core.communication.common.ResolvableNodeId;
import de.rcenvironment.core.communication.management.WorkflowHostService;
import de.rcenvironment.core.component.api.ComponentUtils;
import de.rcenvironment.core.component.api.DistributedComponentKnowledge;
import de.rcenvironment.core.component.api.DistributedComponentKnowledgeService;
import de.rcenvironment.core.component.execution.api.ExecutionControllerException;
import de.rcenvironment.core.component.execution.api.RemotableComponentExecutionControllerService;
import de.rcenvironment.core.component.workflow.api.WorkflowConstants;
import de.rcenvironment.core.component.workflow.execution.api.RemotableWorkflowExecutionControllerService;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowDescriptionValidationResult;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionContext;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionException;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionInformation;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionService;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowFileException;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowState;
import de.rcenvironment.core.component.workflow.execution.spi.WorkflowDescriptionLoaderCallback;
import de.rcenvironment.core.component.workflow.model.api.WorkflowDescription;
import de.rcenvironment.core.component.workflow.model.api.WorkflowDescriptionPersistenceHandler;
import de.rcenvironment.core.component.workflow.model.api.WorkflowNode;
import de.rcenvironment.core.component.workflow.update.api.PersistentWorkflowDescription;
import de.rcenvironment.core.component.workflow.update.api.PersistentWorkflowDescriptionUpdateService;
import de.rcenvironment.core.component.workflow.update.api.PersistentWorkflowDescriptionUpdateUtils;
import de.rcenvironment.core.notification.DistributedNotificationService;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.common.rpc.RemoteOperationException;
import de.rcenvironment.core.utils.incubator.DebugSettings;
import de.rcenvironment.toolkit.modules.concurrency.api.AsyncExceptionListener;
import de.rcenvironment.toolkit.modules.concurrency.api.CallablesGroup;
import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription;
/**
* Implementation of {@link WorkflowExecutionService}.
*
* @author Doreen Seider
* @author Robert Mischke
*/
public class WorkflowExecutionServiceImpl implements WorkflowExecutionService {
private static final String FAILED_TO_LOAD_WORKFLOW_FILE = "Failed to load workflow file: ";
private static final Log LOG = LogFactory.getLog(WorkflowExecutionServiceImpl.class);
/**
* The interval (in msec) between the "heartbeat" notifications sent for active workflows. Workflows are considered active when they are
* running or paused, or in the transitional states in-between.
*/
private static final int ACTIVE_WORKFLOW_HEARTBEAT_NOTIFICATION_INTERVAL_MSEC = 6 * 1000;
private final boolean verboseLogging = DebugSettings.getVerboseLoggingEnabled(getClass());
private CommunicationService communicationService;
private DistributedNotificationService notificationService;
private PersistentWorkflowDescriptionUpdateService wfUpdateService;
private PlatformService platformService;
private WorkflowHostService workflowHostService;
private RemotableWorkflowExecutionControllerService wfExeCtrlService;
private DistributedComponentKnowledgeService componentKnowledgeService;
private RemotableComponentExecutionControllerService componentExecutionControllerService;
private Set<WorkflowExecutionInformation> workflowExecutionInformations;
private Object wfExeFetchLock = new Object();
private ScheduledFuture<?> heartbeatSendFuture;
protected void activate(BundleContext context) {
heartbeatSendFuture = ConcurrencyUtils.getAsyncTaskService().scheduleAtFixedRate(new Runnable() {
@Override
@TaskDescription("Send heartbeat for active workflows")
public void run() {
Set<WorkflowExecutionInformation> wfExeInfoSnapshot = getWorkflowExecutionInformation();
for (WorkflowExecutionInformation wfExeInfo : wfExeInfoSnapshot) {
String wfExeId = wfExeInfo.getExecutionIdentifier();
switch (wfExeInfo.getWorkflowState()) {
case INIT:
case PREPARING:
case STARTING:
case RUNNING:
case PAUSING:
case PAUSED:
case RESUMING:
case CANCELING:
case CANCELING_AFTER_FAILED:
if (verboseLogging) {
LOG.debug(StringUtils.format("Sending heartbeat notification for active workflow '%s' (%s)",
wfExeInfo.getInstanceName(), wfExeId));
}
notificationService.send(WorkflowConstants.STATE_NOTIFICATION_ID + wfExeId, WorkflowState.IS_ALIVE.name());
break;
default:
// do nothing
break;
}
}
}
private Set<WorkflowExecutionInformation> getWorkflowExecutionInformation() {
Set<WorkflowExecutionInformation> wfExeInfoSnapshot = new HashSet<>();
try {
wfExeInfoSnapshot.addAll(wfExeCtrlService.getWorkflowExecutionInformations());
} catch (ExecutionControllerException | RemoteOperationException e) {
LOG.error("Failed to fetch local workflow execution informations: " + e.getMessage());
}
return wfExeInfoSnapshot;
}
}, ACTIVE_WORKFLOW_HEARTBEAT_NOTIFICATION_INTERVAL_MSEC);
}
protected void deactivate() {
if (heartbeatSendFuture != null) {
heartbeatSendFuture.cancel(true);
}
}
@Override
public WorkflowDescription loadWorkflowDescriptionFromFileConsideringUpdates(File wfFile, WorkflowDescriptionLoaderCallback callback)
throws WorkflowFileException {
// delegate
return loadWorkflowDescriptionFromFileConsideringUpdates(wfFile, callback, false);
}
@Override
public WorkflowDescription loadWorkflowDescriptionFromFileConsideringUpdates(File wfFile, WorkflowDescriptionLoaderCallback callback,
boolean abortIfWorkflowUpdateRequired) throws WorkflowFileException {
try {
int wfVersion = readWorkflowVersionNumber(wfFile);
if (wfVersion > WorkflowConstants.CURRENT_WORKFLOW_VERSION_NUMBER) {
throw new WorkflowFileException(FAILED_TO_LOAD_WORKFLOW_FILE + wfFile.getAbsolutePath()
+ StringUtils.format(". Its version (%d) is newer than the expected"
+ " one (%d). Most likely reason: it was opened with a newer version of RCE before.",
wfVersion, WorkflowConstants.CURRENT_WORKFLOW_VERSION_NUMBER));
}
try (InputStream fileInputStream = new FileInputStream(wfFile)) {
PersistentWorkflowDescription persistentDescription = wfUpdateService.createPersistentWorkflowDescription(
IOUtils.toString(fileInputStream, WorkflowConstants.ENCODING_UTF8));
boolean updateRequired = wfUpdateService.isUpdateForWorkflowDescriptionAvailable(persistentDescription, false);
boolean nonSilentUpdateRequired = updateRequired;
if (updateRequired && abortIfWorkflowUpdateRequired) {
throw new WorkflowFileException(
"The workflow file "
+ wfFile.getAbsolutePath()
+ " would require an update before execution, but the 'fail on required update' flag has been set. "
+ "Typically, this means that it was generated from an internal template which should be updated.");
}
if (!nonSilentUpdateRequired) {
updateRequired = wfUpdateService.isUpdateForWorkflowDescriptionAvailable(persistentDescription, true);
}
if (updateRequired) {
String backupFilename = null;
if (nonSilentUpdateRequired) {
backupFilename = PersistentWorkflowDescriptionUpdateUtils.getFilenameForBackupFile(wfFile) + ".wf";
FileUtils.copyFile(wfFile, new File(wfFile.getParentFile().getAbsolutePath(), backupFilename));
}
try {
updateWorkflow(persistentDescription, wfFile, nonSilentUpdateRequired);
onWorkflowFileUpdated(wfFile, !nonSilentUpdateRequired, backupFilename, callback);
} catch (IOException | RuntimeException e) {
if (nonSilentUpdateRequired) {
throw new WorkflowFileException(StringUtils.format("Failed to update workflow file: %s. Backup file "
+ "was generated: %s.", wfFile.getAbsolutePath(), backupFilename), e);
} else {
throw new WorkflowFileException(StringUtils.format("Failed to update workflow file: %s.",
wfFile.getAbsolutePath()), e);
}
}
}
}
return loadWorkflowDescriptionFromFile(wfFile, callback);
} catch (IOException | ParseException e) {
throw new WorkflowFileException(FAILED_TO_LOAD_WORKFLOW_FILE + wfFile.getAbsolutePath(), e);
}
}
private int readWorkflowVersionNumber(File wfFile) throws ParseException, IOException {
try (InputStream fileInputStream = new FileInputStream(wfFile)) {
return new WorkflowDescriptionPersistenceHandler().readWorkflowVersionNumber(fileInputStream);
}
}
@Override
public WorkflowDescription loadWorkflowDescriptionFromFile(File wfFile, WorkflowDescriptionLoaderCallback callback)
throws WorkflowFileException {
try {
int wfVersion = readWorkflowVersionNumber(wfFile);
if (wfVersion > WorkflowConstants.CURRENT_WORKFLOW_VERSION_NUMBER) {
throw new WorkflowFileException(FAILED_TO_LOAD_WORKFLOW_FILE + wfFile.getAbsolutePath()
+ StringUtils.format(". Its version (%d) is older than the expected"
+ " one (%d). Most likely reason: Internal error on workflow update.",
wfVersion, WorkflowConstants.CURRENT_WORKFLOW_VERSION_NUMBER));
}
WorkflowDescriptionPersistenceHandler wdPesistenceHandler = new WorkflowDescriptionPersistenceHandler();
WorkflowDescription wd;
try (InputStream fileInputStream = new FileInputStream(wfFile)) {
wd = wdPesistenceHandler.readWorkflowDescriptionFromStream(fileInputStream);
} catch (WorkflowFileException e) {
if (e.getParsedWorkflowDescription() != null && callback.arePartlyParsedWorkflowConsiderValid()) {
// backup the orginal workflow file and overwrite it with the reduced but valid workflow description
String backupFilename =
PersistentWorkflowDescriptionUpdateUtils.getFilenameForBackupFile(wfFile) + WorkflowConstants.WORKFLOW_FILE_ENDING;
FileUtils.copyFile(wfFile, new File(wfFile.getParentFile().getAbsolutePath(), backupFilename));
wd = e.getParsedWorkflowDescription();
try (FileOutputStream fos = new FileOutputStream(wfFile);
ByteArrayOutputStream baos = wdPesistenceHandler.writeWorkflowDescriptionToStream(wd)) {
baos.writeTo(fos);
}
callback.onWorkflowFileParsingPartlyFailed(backupFilename);
} else {
throw e;
}
}
return wd;
} catch (IOException | ParseException | RuntimeException e) {
throw new WorkflowFileException(FAILED_TO_LOAD_WORKFLOW_FILE + wfFile.getAbsolutePath(), e);
}
}
private void onWorkflowFileUpdated(File wfFile, boolean silentUpdate, String backupFilename,
WorkflowDescriptionLoaderCallback callback) {
if (silentUpdate) {
String message = StringUtils.format("'%s' is updated (silent) (full path: %s)", wfFile.getName(), wfFile.getAbsolutePath());
LOG.debug(message);
callback.onSilentWorkflowFileUpdated(message);
} else {
String message =
StringUtils.format("'%s' is updated (non-silent); backup file generated: %s (full path: %s)", wfFile.getName(),
backupFilename, wfFile.getAbsolutePath());
LOG.debug(message);
callback.onNonSilentWorkflowFileUpdated(message, backupFilename);
}
}
private void updateWorkflow(PersistentWorkflowDescription persWfDescr, File file, boolean hasNonSilentUpdate) throws IOException {
try (InputStream tempInputStream = IOUtils.toInputStream(wfUpdateService
.performWorkflowDescriptionUpdate(persWfDescr).getWorkflowDescriptionAsString(), WorkflowConstants.ENCODING_UTF8)) {
FileUtils.write(file, IOUtils.toString(tempInputStream));
tempInputStream.close();
}
}
@Override
public WorkflowDescriptionValidationResult validateWorkflowDescription(WorkflowDescription workflowDescription) {
LogicalNodeId missingControllerNodeId = null;
Map<String, LogicalNodeId> missingComponentsNodeIds = new HashMap<>();
LogicalNodeId controllerNode = workflowDescription.getControllerNode();
if (controllerNode == null) {
controllerNode = platformService.getLocalDefaultLogicalNodeId();
}
if (!workflowHostService.getLogicalWorkflowHostNodesAndSelf().contains(controllerNode)) {
missingControllerNodeId = controllerNode;
}
DistributedComponentKnowledge compKnowledge = componentKnowledgeService.getCurrentComponentKnowledge();
for (WorkflowNode node : workflowDescription.getWorkflowNodes()) {
LogicalNodeId componentNode = node.getComponentDescription().getNode();
if (componentNode == null) {
componentNode = platformService.getLocalDefaultLogicalNodeId();
}
if (!ComponentUtils.hasComponent(compKnowledge.getAllInstallations(), node.getComponentDescription().getIdentifier(),
componentNode)) {
missingComponentsNodeIds.put(node.getName(), componentNode);
}
}
if (missingControllerNodeId == null && missingComponentsNodeIds.isEmpty()) {
return WorkflowDescriptionValidationResult.createResultForSuccess();
} else {
return WorkflowDescriptionValidationResult.createResultForFailure(missingControllerNodeId, missingComponentsNodeIds);
}
}
@Override
public WorkflowExecutionInformation executeWorkflowAsync(WorkflowExecutionContext wfExeCtx)
throws WorkflowExecutionException, RemoteOperationException {
WorkflowExecutionInformation workflowExecutionInformation = createExecutionController(wfExeCtx);
try {
performStartOnExecutionController(workflowExecutionInformation.getExecutionIdentifier(), wfExeCtx.getNodeId());
} catch (ExecutionControllerException e) {
throw new WorkflowExecutionException("Failed to execute workflow", e);
}
return workflowExecutionInformation;
}
private WorkflowExecutionInformation createExecutionController(WorkflowExecutionContext wfExeCtx) throws RemoteOperationException,
WorkflowExecutionException {
Map<String, String> authTokens = createAndRegisterLocalComponentExecutionAuthTokens((wfExeCtx).getWorkflowDescription());
return getExecutionControllerService(wfExeCtx.getNodeId()).createExecutionController(wfExeCtx, authTokens,
!platformService.matchesLocalInstance(wfExeCtx.getNodeId()));
}
/**
* Creates an auth token for each component which must be instantiated locally from an remote workflow execution controller that was
* instantiated from local node. It ensures that local components, which were not published, can be instantiated from remote, but only
* from workflow execution controllers, which were created from local node and thus, which are allowed to instantiate local components
* even if they are not published.
*/
private Map<String, String> createAndRegisterLocalComponentExecutionAuthTokens(WorkflowDescription workflowDescription) {
Map<String, String> compIdToTokenMapping = new HashMap<String, String>();
for (WorkflowNode wfNode : workflowDescription.getWorkflowNodes()) {
LogicalNodeId node = wfNode.getComponentDescription().getNode();
// Use empty string instead of null to avoid "remote method not found" issue. If null is
// passed the method can not be inspected
String token = "";
if (node == null || platformService.matchesLocalInstance(node)) {
token = UUID.randomUUID().toString();
try {
componentExecutionControllerService.addComponentExecutionAuthToken(token);
} catch (RemoteOperationException e) {
// should not happen as it is finally local call
throw new IllegalStateException("Failed to add auth tokens for component execution; cause: " + e.toString());
}
}
compIdToTokenMapping.put(wfNode.getIdentifier(), token);
}
return compIdToTokenMapping;
}
private void performStartOnExecutionController(String executionId, ResolvableNodeId node) throws ExecutionControllerException,
RemoteOperationException {
getExecutionControllerService(node).performStart(executionId);
}
@Override
public void cancel(String executionId, ResolvableNodeId node) throws ExecutionControllerException, RemoteOperationException {
getExecutionControllerService(node).performCancel(executionId);
}
@Override
public void pause(String executionId, ResolvableNodeId node) throws ExecutionControllerException, RemoteOperationException {
getExecutionControllerService(node).performPause(executionId);
}
@Override
public void resume(String executionId, ResolvableNodeId node) throws ExecutionControllerException, RemoteOperationException {
getExecutionControllerService(node).performResume(executionId);
}
@Override
public void dispose(String executionId, ResolvableNodeId node) throws ExecutionControllerException, RemoteOperationException {
getExecutionControllerService(node).performDispose(executionId);
}
@Override
public WorkflowState getWorkflowState(String executionId, ResolvableNodeId node) throws ExecutionControllerException,
RemoteOperationException {
return getExecutionControllerService(node).getWorkflowState(executionId);
}
@Override
public Long getWorkflowDataManagementId(String executionId, ResolvableNodeId node) throws ExecutionControllerException,
RemoteOperationException {
return getExecutionControllerService(node).getWorkflowDataManagementId(executionId);
}
@Override
public Set<WorkflowExecutionInformation> getLocalWorkflowExecutionInformations() {
try {
return new HashSet<WorkflowExecutionInformation>(wfExeCtrlService.getWorkflowExecutionInformations());
} catch (ExecutionControllerException | RemoteOperationException e) {
// should not happen as it is finally a local call and the ExecutionController are directly fetched before
throw new IllegalStateException("Failed to get local workflow execution information; cause: " + e.toString());
}
}
@Override
public Set<WorkflowExecutionInformation> getWorkflowExecutionInformations() {
return getWorkflowExecutionInformations(false);
}
@Override
public Set<WorkflowExecutionInformation> getWorkflowExecutionInformations(boolean forceRefresh) {
if (!forceRefresh && workflowExecutionInformations != null) {
return new HashSet<>(workflowExecutionInformations);
} else {
synchronized (wfExeFetchLock) {
if (forceRefresh || workflowExecutionInformations == null) {
Set<WorkflowExecutionInformation> tempWfExeInfos = new HashSet<>();
CallablesGroup<Collection> callablesGroup =
ConcurrencyUtils.getFactory().createCallablesGroup(Collection.class);
for (InstanceNodeSessionId node : workflowHostService.getWorkflowHostNodesAndSelf()) {
final InstanceNodeSessionId finalNode = node;
callablesGroup.add(new Callable<Collection>() {
@Override
@TaskDescription("Distributed query: getWorkflowInformations()")
public Collection call() throws Exception {
RemotableWorkflowExecutionControllerService executionControllerService =
getExecutionControllerService(finalNode);
try {
return executionControllerService.getWorkflowExecutionInformations();
} catch (RemoteOperationException e) {
LOG.error(StringUtils.format("Failed to query remote workflows on node %s; cause: %s",
finalNode, e.toString()));
}
return null;
}
});
}
List<Collection> results = callablesGroup.executeParallel(new AsyncExceptionListener() {
@Override
public void onAsyncException(Exception e) {
LOG.warn("Exception during asynchrous execution", e);
}
});
// merge results
for (Collection singleResult : results) {
if (singleResult != null) {
tempWfExeInfos.addAll(singleResult);
}
}
workflowExecutionInformations = tempWfExeInfos;
}
return new HashSet<>(workflowExecutionInformations);
}
}
}
private RemotableWorkflowExecutionControllerService getExecutionControllerService(ResolvableNodeId node)
throws RemoteOperationException {
// fetching the service proxy on each call, assuming that it will be cached centrally if necessary
return communicationService.getRemotableService(RemotableWorkflowExecutionControllerService.class, node);
}
protected void bindCommunicationService(CommunicationService newService) {
communicationService = newService;
}
protected void bindNotificationService(DistributedNotificationService newService) {
notificationService = newService;
}
protected void bindPersistentWorkflowDescriptionUpdateService(PersistentWorkflowDescriptionUpdateService newService) {
wfUpdateService = newService;
}
protected void bindPlatformService(PlatformService newService) {
platformService = newService;
}
protected void bindComponentExecutionControllerService(RemotableComponentExecutionControllerService newService) {
componentExecutionControllerService = newService;
}
protected void bindWorkflowHostService(WorkflowHostService newService) {
workflowHostService = newService;
}
protected void bindDistributedComponentKnowledgeService(DistributedComponentKnowledgeService newService) {
componentKnowledgeService = newService;
}
protected void bindWorkflowExecutionControllerService(RemotableWorkflowExecutionControllerService newService) {
wfExeCtrlService = newService;
}
}