/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.workflow;
import static com.google.common.collect.Collections2.filter;
import static com.google.common.collect.Lists.newArrayList;
import java.io.ObjectStreamField;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.coordinator.client.service.impl.GenericSerializer;
import com.emc.storageos.db.client.model.Operation;
import com.emc.storageos.svcs.errorhandling.model.ServiceError;
import com.emc.storageos.svcs.errorhandling.resources.ServiceCode;
import com.emc.storageos.volumecontroller.TaskCompleter;
import com.google.common.base.Predicate;
/**
* A Workflow represents a sequence of steps that can be executed by Controllers to
* achieve a higher level goal.
*
* @author Watson 2/26/2013 revised 3/1/2013
*/
public class Workflow implements Serializable {
// State variables in the Workflow, should not be used by clients.
private static final long serialVersionUID = -7097852372832495267L;
WorkflowService _service; // WorkflowService manages this Workflow
String _orchControllerName; // simple name of the Orchestration Controller
String _orchMethod; // Orchestration Method Name we are working
String _orchTaskId; // Orchestration taskId
String _successMessage; // Message that will be emitted if successful completion
Boolean _rollbackContOnError; // Rollback continues even if a rollback step fails
Boolean _rollbackState = false; // this workflow is in a rollback state
// The current steps in the Workflow
Map<String, Step> _stepMap = new HashMap<String, Step>();
// maps stepGroup to step id list
Map<String, Set<String>> _stepGroupMap = new HashMap<String, Set<String>>();
Map<String, StepStatus> _stepStatusMap = new LinkedHashMap<String, StepStatus>();
WorkflowCallbackHandler _callbackHandler;// a callback handler
Object[] _callbackHandlerArgs; // arguments for the callback handlers (serializable)
WorkflowRollbackHandler _rollbackHandler; // rollback handlers
Object[] _rollbackHandlerArgs; // arguments for the rollback handlers (serializable)
TaskCompleter _taskCompleter; // task completer to be called at end of workflow
Boolean _nested = false; // this workflow is nested, run from within another workflow
URI _workflowURI; // URI for Cassandra logging record
Set<URI> _childWorkflows = new HashSet<URI>(); // workflowURI of child workflows
Boolean _suspendOnError = true; // suspend on error (rather than rollback)
private WorkflowState _workflowState;
Set<URI> _suspendSteps = new HashSet<URI>(); // Steps that initiate workflow suspend
private Boolean _rollingBackFromSuspend = false; // Set during rollback initiated from suspend, transient
private Boolean _treatSuspendRollbackAsTerminate = false;
// Define the serializable, persistent fields save in ZK
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("_orchControllerName", String.class),
new ObjectStreamField("_orchMethod", String.class),
new ObjectStreamField("_orchTaskId", String.class),
new ObjectStreamField("_successMessage", String.class),
new ObjectStreamField("_callbackHandler", WorkflowCallbackHandler.class),
new ObjectStreamField("_callbackHandlerArgs", Object[].class),
new ObjectStreamField("_rollbackHandler", WorkflowRollbackHandler.class),
new ObjectStreamField("_rollbackHandlerArgs", Object[].class),
new ObjectStreamField("_taskCompleter", TaskCompleter.class),
new ObjectStreamField("_stepGroupMap", Map.class),
new ObjectStreamField("_rollbackContOnError", Boolean.class),
new ObjectStreamField("_rollbackState", Boolean.class),
new ObjectStreamField("_workflowURI", URI.class),
new ObjectStreamField("_childWorkflows", Set.class),
new ObjectStreamField("_nested", Boolean.class),
new ObjectStreamField("_stepMap", Map.class),
new ObjectStreamField("_stepStatusMap", Map.class),
new ObjectStreamField("_suspendOnError", Boolean.class),
new ObjectStreamField("_workflowState", WorkflowState.class),
new ObjectStreamField("_suspendSteps", Set.class),
new ObjectStreamField("_treatSuspendRollbackAsTerminate", Boolean.class)
};
private static final Logger _log = LoggerFactory.getLogger(Workflow.class);
/**
* The state of a Step.
*/
static public class Step implements Serializable {
private static final long serialVersionUID = -3350633739681733597L;
/** A unique UUID string identifying each step. */
public String stepId;
/** A human readable description of what the step does. */
public String description;
/** Every step belongs to a StepGroup. This is the stepGroup name. */
public String stepGroup;
/**
* If non-null, a stepId or stepGroup name that must complete before
* this step executes.
*/
public String waitFor;
/** The underlying device URI for this step (e.g. StorageSystem). */
public URI deviceURI;
/** The device type (e.g. device.getType() ) */
public String deviceType;
/** tells the dispatcher whether to take out a semaphore lock on the device. */
public boolean lockDevice;
/** The name of the controller used to execute the step. */
public String controllerName;
/** The method parameters used to execute the step. */
public Method executeMethod;
/** The Method parameters to rollback the initial call. */
public Method rollbackMethod;
/** The current status of the step. */
public StepStatus status;
/** URI of Cassandra logging record. */
public URI workflowStepURI;
/** Is this a rollback step? */
public boolean isRollbackStep = false;
/** Rollback steps only: Step ID of the step that created this step */
public String foundingStepId;
/** Mark this step for suspend **/
public boolean suspendStep = false;
/** Special message to be displayed if step is suspended. */
public String suspendedMessage = null;
/**
* Created COP-37 to track hashCode() implementation in this class.
*/
@SuppressWarnings({ "squid:S1206" })
public boolean equals(Object o) {
if (o == null || !(o instanceof Step)) {
return false;
}
Step other = (Step) o;
return this.stepId.equalsIgnoreCase(other.stepId);
}
// Rollback steps are in the rollback step group.
public static final String ROLLBACK_GROUP = "_rollback_group_";
public boolean isRollbackStep() {
return ROLLBACK_GROUP.equalsIgnoreCase(this.stepGroup);
}
/**
* Generates a rollback Step corresponding to this step.
*
* @return Step
*/
public Step generateRollbackStep() {
Step rb = new Step();
rb.stepId = UUID.randomUUID().toString() + UUID.randomUUID().toString();
rb.description = "Rollback " + this.description;
rb.isRollbackStep = true;
rb.waitFor = null;
rb.deviceURI = this.deviceURI;
rb.deviceType = this.deviceType;
rb.controllerName = this.controllerName;
rb.executeMethod = this.rollbackMethod;
rb.stepGroup = ROLLBACK_GROUP;
rb.foundingStepId = this.stepId;
return rb;
}
};
/**
* The States a Step may be in.
*/
public enum StepState implements Serializable {
/** step has been created, but not queued */
CREATED,
/** step is waiting on a prerequisite step to complete before dispatching. */
BLOCKED,
/** step has been queued to the Dispatcher, but no reported execution */
QUEUED,
/** step has been reported as executing */
EXECUTING,
/** step was cancelled due to failure of a step this step was dependent on (terminal state) */
CANCELLED,
/** step was successfully completed (terminal state) */
SUCCESS,
/** step completed in error (terminal state) */
ERROR,
/** step suspended, no error (user requested or system requested */
SUSPENDED_NO_ERROR,
/** step suspended due to an error */
SUSPENDED_ERROR;
/** Returns the equivalent Operation.Status value */
public Operation.Status getOperationStatus() {
if (this == SUCCESS) {
return Operation.Status.ready;
}
if (this == CANCELLED || this == ERROR) {
return Operation.Status.error;
}
return Operation.Status.pending;
}
public ServiceCode getServiceCode() {
switch (this) {
case CANCELLED:
return ServiceCode.WORKFLOW_STEP_CANCELLED;
case ERROR:
return ServiceCode.WORKFLOW_STEP_ERROR;
default:
return null;
}
}
}
/**
* The current status of a Step. This is similar to, but not dependent on, Operation.
*/
static public class StepStatus implements Serializable {
private static final long serialVersionUID = -9046185136835089364L;
/** String UUID step identifier */
public String stepId;
/** The current StepState */
public StepState state;
/** A message from the underlying controller */
public String message;
/** Human readable description of what this step is doing */
public String description;
/** Time step was queued to the Dispatcher */
public Date startTime;
/** Time step reached a terminal state */
public Date endTime;
/** The service code for an error state */
public ServiceCode serviceCode;
StepStatus() {
}
StepStatus(String stepId, StepState state, String description) {
this.stepId = stepId;
this.state = state;
this.description = description;
this.state = StepState.CREATED;
}
/**
* @return True if the step is in a Terminal State, i.e. CANCELLED, ERROR, or SUCCESS
*/
boolean isTerminalState() {
return (state == StepState.CANCELLED || state == StepState.SUSPENDED_NO_ERROR || state == StepState.SUSPENDED_ERROR
|| state == StepState.ERROR || state == StepState.SUCCESS);
}
ServiceError getServiceError() {
return ServiceError.buildServiceError(serviceCode, message);
}
/**
* Called to update the state when a callback message is received in
* Zookeeper as a result of a WorkflowStepCompleter.updateState().
* Note that if there are any threads waiting on this step, they will be
* awakened so that they will recheck the Step's status.
*
* @param newState
* The new state reported.
* @param message
* Message from the controller.
*/
synchronized void updateState(StepState newState, ServiceCode code, String message) {
if (this.state == StepState.SUSPENDED_ERROR && newState == StepState.ERROR) {
// If the current step state is SUSPENDED_ERROR and the new state is ERROR,
// then rollback of that suspended step has been initiated and we are moving the
// state of the execution step to error. In this case, we don't want override
// the code and message with the passed values, as these come from the rollback
// step state. So, we set just the new state and end time. Additionally, we
// don't want the suspended message to be part of the final error message for
// the step, so we extract that part of the message so we only see the actual
// error that caused the suspension when rollback completes.
this.state = newState;
this.endTime = new Date();
String suspendedMsg = String.format("Message: %s", WorkflowService.SUSPENDED_MSG);
this.message = this.message.substring(suspendedMsg.length());
} else {
this.state = newState;
this.serviceCode = code;
this.message = message;
if (newState == StepState.QUEUED || newState == StepState.CANCELLED) {
this.startTime = new Date();
}
if (newState == StepState.CANCELLED
|| newState == StepState.SUCCESS || newState == StepState.ERROR) {
this.endTime = new Date();
}
// SUSPENDED state doesn't need either start nor endTime specified
this.serviceCode = code == null ? newState.getServiceCode() : code;
}
this.notifyAll();
}
/**
* Block the calling thread until this step reaches a terminal state.
*/
synchronized void waitForTerminalState() {
while (this.state == StepState.BLOCKED
|| this.state == StepState.QUEUED
|| this.state == StepState.EXECUTING) {
try {
this.wait(600000); // 600 seconds, or 10 minutes
} catch (InterruptedException ex) {
_log.error(ex.getMessage(), ex);
}
}
}
}
/**
* Represents a method that can be called by the Workflow.
* It would be an executeMethod or a rollbackMethod.
*
*/
static public class Method implements Serializable {
/** The methodName for this method. This is the function that will be called. */
String methodName;
/** Arbitrary arguments for the method. Must be serializable. */
Object[] args;
public Method(String methodName, Object... args) {
this.methodName = methodName;
this.args = args;
}
public void checkSerialization() throws WorkflowException {
byte[] bytes = GenericSerializer.serialize(this, methodName, false);
}
}
/**
* Defines a NULL method, either for execution or rollback. The null method always returns "Step Succeeded.".
*/
static final public Method NULL_METHOD = new Workflow.Method("_null_method_");
/**
* The interface that must be provided as the workflow callback handler.
*/
public interface WorkflowCallbackHandler {
/** The workflow is completed. */
public void workflowComplete(Workflow workflow, Object[] args) throws WorkflowException;
}
/**
* The interface that must be provided for rollback callback handler.
*/
public interface WorkflowRollbackHandler {
/** The workflow is initiating rollback. */
public void initiatingRollback(Workflow workflow, Object[] args);
/** The workflow has completed rollback. */
public void rollbackComplete(Workflow workflow, Object[] args);
}
/**
* Constructor to be called by WorkflowService. NOT TO BE CALLED BY CLIENTS.
*
* @param service
* - Handle to the WorkflowService
* @param orchControllerName
* - The simple name of the Controller on behalf this Workflow is executing.
* @param methodName
* - Method within the controller on
* @param rollbackContOnError
* -- Run all rollback steps, even if a rollback ERRORs?
* @param stepId
* - String taskId (UUID) representing this specific orcestration instance.
* @param workflowURI
* - URI desired for workflow, or can be passed as null and will be generated
*/
Workflow(WorkflowService service, String orchControllerName, String methodName, String taskId, URI workflowURI) {
_service = service;
_orchControllerName = orchControllerName;
_orchMethod = methodName;
_orchTaskId = taskId;
_workflowURI = workflowURI;
_workflowState = WorkflowState.CREATED;
}
/**
* Constructor to be called by WorkflowService. NOT TO BE CALLED BY CLIENTS.
* Used to locate Workflows by their URI.
*
* @param service
* - Handle to the WorkflowService
* @param orchControllerName
* - The simple name of the Controller on behalf this Workflow is executing.
* @param methodName
* - Method within the controller on
* @param workflowURI
* - URI of existing Workflow
*/
Workflow(WorkflowService service, String orchControllerName, String methodName, URI workflowURI) {
_service = service;
_orchControllerName = orchControllerName;
_orchMethod = methodName;
_workflowURI = workflowURI;
_workflowState = WorkflowState.CREATED;
}
/**
* Destroys the persisted (Zookeeper) state of this Workflow.
* This is a destructor called from the WorkflowController.
*/
void destroyWorkflow() {
_service.destroyWorkflow(this);
}
/**
* Check that the method is defined in the controller.
*
* @param controller
* @param methodName
* @throws WorkflowException
*/
private void methodNameValidator(Class controllerClass, String methodName)
throws WorkflowException {
if (methodName.equals(NULL_METHOD.methodName)) {
return;
}
java.lang.reflect.Method[] methods = controllerClass.getMethods();
for (java.lang.reflect.Method method : methods) {
if (method.getName().equals(methodName)) {
return;
}
}
throw new WorkflowException(String.format(
"In class %s there is no method matching %s",
controllerClass.getSimpleName(), methodName));
}
/**
* Creates a step id for use with createStep().
*
* @return String stepId
*/
public String createStepId() {
return UUID.randomUUID().toString() + UUID.randomUUID().toString();
}
/**
* Creates a step for execution on an internal Queue within the Workflow and
* returns after internally generating a step UUID for the step. The step
* is not executable until one or methods have been set
* (setExecutableMethod or setRollbackMethod) and the Workflow.execute()
* call has been initiated.
*
* <p>
*
* @param stepGroup
* -- Step group name this step is a member of. Other steps can
* wait until all the steps in the specified stepGroup have
* completed. Do not use UUID values for stepGroup names.
* You can pass null if this step should not belong to any
* step groups.
* @param description
* -- Short textual description of the step for logging/status
* displays.
* @param waitFor
* -- If non-null, the step will not be queued for execution in
* the Dispatcher until the Step or StepGroup indicated by the
* waitFor has completed. The waitFor may either be a string
* representation of a Step UUID, or the name of a StepGroup.
* @param deviceURI
* -- The URI of the affected device, e.g. StorageSystem or
* NetworkSystem. This is a required parameter to the Dispatcher,
* who maintains a semaphore count on the number of outstanding
* operations to each device instance.
* @param deviceType
* --The type of Device, used to find the controller. Typically
* given by device.getDeviceType(). This is a required
* parameter to the Dispatcher.
* @param controllerClass
* -- The controller class (like
* NetworkDeviceController.class, BlockDeviceController.class)
* @param executeMethod
* - Method name and parameters for the execution method.
* @param rollbackMethod
* - Method name name parameters for the rollback method.
* @param stepId
* - If non null, specifies the stepId to be used, otherwise if null a stepId is
* automatically generated.
*
* @return String representing UUID of generated step
*/
public String createStep(String stepGroup, String description,
String waitFor, URI deviceURI, String deviceType,
Class controllerClass, Method executeMethod, Method rollbackMethod,
String stepId)
throws WorkflowException {
return createStep(stepGroup, description, waitFor, deviceURI, deviceType, true, controllerClass,
executeMethod, rollbackMethod, false, stepId);
}
/**
* Creates a step for execution on an internal Queue within the Workflow and
* returns after internally generating a step UUID for the step. The step
* is not executable until one or methods have been set
* (setExecutableMethod or setRollbackMethod) and the Workflow.execute()
* call has been initiated.
*
* <p>
*
* @param stepGroup
* -- Step group name this step is a member of. Other steps can
* wait until all the steps in the specified stepGroup have
* completed. Do not use UUID values for stepGroup names.
* You can pass null if this step should not belong to any
* step groups.
* @param description
* -- Short textual description of the step for logging/status
* displays.
* @param waitFor
* -- If non-null, the step will not be queued for execution in
* the Dispatcher until the Step or StepGroup indicated by the
* waitFor has completed. The waitFor may either be a string
* representation of a Step UUID, or the name of a StepGroup.
* @param deviceURI
* -- The URI of the affected device, e.g. StorageSystem or
* NetworkSystem. This is a required parameter to the Dispatcher,
* who maintains a semaphore count on the number of outstanding
* operations to each device instance.
* @param deviceType
* --The type of Device, used to find the controller. Typically
* given by device.getDeviceType(). This is a required
* parameter to the Dispatcher.
* @param controllerClass
* -- The controller class (like
* NetworkDeviceController.class, BlockDeviceController.class)
* @param executeMethod
* - Method name and parameters for the execution method.
* @param rollbackMethod
* - Method name name parameters for the rollback method.
* @param suspendStep
* - suspend this step
* @param stepId
* - If non null, specifies the stepId to be used, otherwise if null a stepId is
* automatically generated.
*
* @return String representing UUID of generated step
*/
public String createStep(String stepGroup, String description,
String waitFor, URI deviceURI, String deviceType,
Class controllerClass, Method executeMethod, Method rollbackMethod,
boolean suspendStep, String stepId)
throws WorkflowException {
return createStep(stepGroup, description, waitFor, deviceURI, deviceType, true, controllerClass,
executeMethod, rollbackMethod, suspendStep, stepId);
}
/**
* Creates a step for execution on an internal Queue within the Workflow and
* returns after internally generating a step UUID for the step. The step
* is not executable until one or methods have been set
* (setExecutableMethod or setRollbackMethod) and the Workflow.execute()
* call has been initiated.
*
* <p>
*
* @param stepGroup
* -- Step group name this step is a member of. Other steps can
* wait until all the steps in the specified stepGroup have
* completed. Do not use UUID values for stepGroup names.
* You can pass null if this step should not belong to any
* step groups.
* @param description
* -- Short textual description of the step for logging/status
* displays.
* @param waitFor
* -- If non-null, the step will not be queued for execution in
* the Dispatcher until the Step or StepGroup indicated by the
* waitFor has completed. The waitFor may either be a string
* representation of a Step UUID, or the name of a StepGroup.
* @param deviceURI
* -- The URI of the affected device, e.g. StorageSystem or
* NetworkSystem. This is a required parameter to the Dispatcher,
* who maintains a semaphore count on the number of outstanding
* operations to each device instance.
* @param deviceType
* --The type of Device, used to find the controller. Typically
* given by device.getDeviceType(). This is a required
* parameter to the Dispatcher.
* @param lockDevice
* tells the dispatcher whether to acquire a semaphore on the device
* before executing the step
* @param controllerClass
* -- The controller class (like
* NetworkDeviceController.class, BlockDeviceController.class)
* @param executeMethod
* - Method name and parameters for the execution method.
* @param rollbackMethod
* - Method name name parameters for the rollback method.
* @param suspend
* - suspend this step at the beginning
* @param stepId
* - If non null, specifies the stepId to be used, otherwise if null a stepId is
* automatically generated.
* @return String representing UUID of generated step
*/
public String createStep(String stepGroup, String description,
String waitFor, URI deviceURI, String deviceType, boolean lockDevice,
Class controllerClass, Method executeMethod, Method rollbackMethod,
boolean suspend, String stepId)
throws WorkflowException {
try {
// Initialize the new step.
Step step = new Step();
if (stepId == null) {
stepId = createStepId();
}
step.stepId = stepId;
step.stepGroup = stepGroup;
step.description = description;
step.waitFor = waitFor;
step.deviceURI = deviceURI;
step.deviceType = deviceType;
step.lockDevice = lockDevice;
step.suspendStep = suspend;
step.controllerName = controllerClass.getName();
// Make a StepStatus entry for it with CREATED status
step.status = new StepStatus(stepId, StepState.CREATED, description);
// Save it in our local structures, the stepMap, stepStatusMap, and the stepGroup map.
getStepMap().put(step.stepId, step);
if (step.stepGroup != null) {
if (getStepGroupMap().get(step.stepGroup) == null) {
getStepGroupMap().put(step.stepGroup, new HashSet<String>());
}
getStepGroupMap().get(step.stepGroup).add(step.stepId);
}
getStepStatusMap().put(stepId, step.status);
// Check the execution method.
if (executeMethod == null) {
throw new WorkflowException("Must supply an executeMethod argument");
}
methodNameValidator(controllerClass, executeMethod.methodName);
executeMethod.checkSerialization();
step.executeMethod = executeMethod;
// The rollback method is optional...
if (rollbackMethod != null) {
methodNameValidator(controllerClass, rollbackMethod.methodName);
rollbackMethod.checkSerialization();
step.rollbackMethod = rollbackMethod;
}
return step.stepId;
} catch (Exception ex) {
_log.error("Exception trying to create workflow step: " + ex.getMessage());
throw new WorkflowException("Cannot create step", ex);
}
}
/**
* Invokes the WorkflowPlanExecutor to execute this workflow plan.
*/
public void executePlan(TaskCompleter completer, String successMessage,
WorkflowCallbackHandler callbackHandler, Object[] callbackHandlerArgs,
WorkflowRollbackHandler rollbackHandler, Object[] rollbackHandlerArgs)
throws WorkflowException {
this._callbackHandler = callbackHandler;
if (callbackHandlerArgs != null) {
this._callbackHandlerArgs = callbackHandlerArgs.clone();
}
this._rollbackHandler = rollbackHandler;
if (rollbackHandlerArgs != null) {
this._rollbackHandlerArgs = rollbackHandlerArgs.clone();
}
this._taskCompleter = completer;
this._successMessage = successMessage;
_service.executePlan(this);
}
public void executePlan(TaskCompleter completer, String successMessage)
throws WorkflowException {
executePlan(completer, successMessage, null, null, null, null);
}
/**
* Returns the current step status without waiting (i.e. even if it is in
* the pending state). Does not block.
*
* @param stepId
* @return StepStatus
*/
public StepStatus getStepStatus(String stepId) throws WorkflowException {
StepStatus status = getStepStatusMap().get(stepId);
if (status == null) {
throw new WorkflowException("Unknown step: " + stepId);
}
return status;
}
/**
* @return True if all Steps have reached a Terminal State
* @throws WorkflowException
*/
boolean allStatesTerminal() throws WorkflowException {
for (String stepId : getStepMap().keySet()) {
StepStatus status = getStepStatus(stepId);
if (false == status.isTerminalState()) {
return false;
}
}
return true;
}
/**
* Returns a Map of UUID to step Status, even if some of the steps
* have not completed execution. Does not block.
*
* @param stepGroup
* - The String name of a Step Group.
* @return -- A Map of step UUID String to StepStatus structure.
*/
public Map<String, StepStatus> getStepGroupStatus(String stepGroup)
throws WorkflowException {
HashMap<String, StepStatus> map = new HashMap<String, StepStatus>();
Set<String> stepGroupList = getStepGroupMap().get(stepGroup);
if (stepGroupList == null) {
throw new WorkflowException("Couldn't find stepGroup: " + stepGroup);
}
for (String stepId : getStepGroupMap().get(stepGroup)) {
StepStatus status = getStepStatus(stepId);
map.put(stepId, status);
}
return map;
}
/**
* Returns a Map of UUID to step Status for all steps in the Workflow, even if
* some of the steps have not completed execution. Does not block.
*
* @return -- A Map of step UUID String to StepResourceRep structure.
* @throws WorkflowException
*/
public Map<String, StepStatus> getAllStepStatus() throws WorkflowException {
HashMap<String, StepStatus> map = new HashMap<String, StepStatus>();
for (String stepId : getStepMap().keySet()) {
StepStatus status = getStepStatus(stepId);
map.put(stepId, status);
}
return map;
}
/**
* Search through the step map and find out if one of the Step has 'methodName'
* as its Workflow.Method
*
* @param controllerClass
* [IN] - Controller class for the step we're searching for
* @param deviceURI
* [IN] - Device URI for which the step applies
* @param methodName
* [IN] - Workflow.Method.methodName to search for
*
* @return true, if there is a Step with Workflow.Method.methodName == 'methodName'
*/
public boolean stepMethodHasBeenScheduled(Class controllerClass, URI deviceURI, String methodName) {
for (String stepId : getStepMap().keySet()) {
Step step = getStepMap().get(stepId);
Workflow.Method method = step.executeMethod;
if (method.methodName.equals(methodName) && step.controllerName.equals(controllerClass.getName())
&& step.deviceURI.equals(deviceURI)) {
return true;
}
}
return false;
}
/**
* Gets the overall Workflow State based on the underlying step states.
*
* @return WorkflowState
*/
public WorkflowState getWorkflowStateFromSteps() {
String[] errorMessage = new String[1];
StepState stepState = getOverallState(getStepStatusMap(), errorMessage);
switch (stepState) {
case SUCCESS:
return WorkflowState.SUCCESS;
case ERROR:
return WorkflowState.ERROR;
case SUSPENDED_ERROR:
return WorkflowState.SUSPENDED_ERROR;
case SUSPENDED_NO_ERROR:
return WorkflowState.SUSPENDED_NO_ERROR;
case CANCELLED:
return WorkflowState.ERROR;
default:
return getWorkflowState();
}
}
/**
* Given a group of steps, determines an overall state. The precedence is:
* 1. If any step is reporting ERROR, ERROR is returned along with that step's message.
* 2. Otherwise if any step is reporting CANCELLED, CANCELLED is returned along with that step's message.
* 3. Otherwise if any step is not returning a state of SUCCESS, CANCELLED, or ERROR, its state and message are
* returned.
* 4. Otherwise if all steps are returning SUCCESS, SUCCESS is returned with the original contents of errorMessage
* (unless they were null).
*
* @param statusMap
* @param errorMessage
* -- Output parameter - selected error message
* @return SUCCESS if all successful; ERROR for first error; other StepState if there is a non SUCCESS/ERROR
*/
public static StepState getOverallState(Map<String, StepStatus> statusMap,
String[] errorMessage)
throws WorkflowException {
StepState state = StepState.SUCCESS;
StringBuilder buf = new StringBuilder(); // Buffer for error messages
StringBuilder rbuf = new StringBuilder(); // Buffer for rollback error messages
if (errorMessage[0] == null) {
errorMessage[0] = "Operation successful";
}
for (String stepId : statusMap.keySet()) {
StepStatus status = statusMap.get(stepId);
switch (status.state) {
case SUCCESS:
break;
case ERROR:
state = status.state;
if (false == status.description.startsWith("Rollback")) {
// Save non-rollback message
if (buf.length() > 0) {
buf.append("; ");
}
buf.append(status.message);
} else {
// Save rollback message
rbuf.append("; Rollback error: ");
rbuf.append(status.message);
}
break;
case SUSPENDED_NO_ERROR:
case SUSPENDED_ERROR:
if (state != StepState.ERROR) {
state = status.state;
errorMessage[0] = status.message;
}
break;
case CANCELLED: // ERROR and SUSPENDS have higher precedence than CANCELLED
if (state != StepState.ERROR && state != StepState.SUSPENDED_NO_ERROR && state != StepState.SUSPENDED_ERROR) {
state = status.state;
errorMessage[0] = status.message;
}
break;
default: // ERROR and CANCELLED and SUSPENDS have higher precedence than any default state
if (state != StepState.ERROR && state != StepState.CANCELLED && state != StepState.SUSPENDED_NO_ERROR
&& state != StepState.SUSPENDED_ERROR) {
state = status.state;
errorMessage[0] = status.message;
}
break;
}
}
// If there's an error, replace the success message
if (buf.length() > 0) {
errorMessage[0] = buf.toString() + rbuf.toString();
}
return state;
}
/**
* Given a set of steps that executed, what is the overall service error associated with the steps?
* This determined in a priority order as follows:
* 1. The first step that failed (ERROR state) time-wise gets priority (is returned first), otherwise...
* 2. The first step that was put in SUSPENDED_ERROR or SUSPENDED_NO_ERROR state time-wise gets returned,
* otherwise...
* 3. The first step that was CANCELLED is returned (not as likely)
*
* The resulting message is a compiled set of error messages from the root cause and all other steps that
* failed, especially during rollback.
*
* @param statusMap
* step map
* @return Service error that represents the overall service error (root cause)
* @throws WorkflowException
*/
public static ServiceError getOverallServiceError(Map<String, StepStatus> statusMap)
throws WorkflowException {
ServiceError error = null;
StepStatus rootCauseStatus = null;
List<StepStatus> additionalErrors = new ArrayList<>();
final Map<StepState, Integer> errorStatePriorities = new HashMap<>();
errorStatePriorities.put(StepState.ERROR, 0);
errorStatePriorities.put(StepState.SUSPENDED_NO_ERROR, 1);
errorStatePriorities.put(StepState.SUSPENDED_ERROR, 1);
// Filter out non-error statuses.
List<StepStatus> statuses = newArrayList(filter(statusMap.values(), new Predicate<StepStatus>() {
@Override
public boolean apply(StepStatus input) {
return errorStatePriorities.containsKey(input.state);
}
}));
//Sort the statuses based on the errorStatePriorities map and also by earliest time.
Collections.sort(statuses, new Comparator<StepStatus>() {
@Override
public int compare(StepStatus a, StepStatus b) {
Integer aStatePriority = errorStatePriorities.get(a.state);
Integer bStatePriority = errorStatePriorities.get(b.state);
if (aStatePriority != null && bStatePriority == null) {
return -1;
} else if (aStatePriority == null && bStatePriority != null) {
return 1;
} else if (aStatePriority == null && bStatePriority == null) {
return 0;
}
int result = aStatePriority.compareTo(bStatePriority);
if (result != 0) {
return result;
}
Date aTime = a.endTime;
Date bTime = b.endTime;
// Comparison based on end time is as follows:
// If a step hasn't finished yet, its endTime is null.
// That time is interpreted as "infinity" for comparison purposes below.
if (aTime != null && bTime == null) {
return -1; // b hasn't ended, so b has an infinite time in comparison
} else if (aTime == null && bTime != null) {
return 1; // a hasn't ended, so a is greater than b
} else if (aTime == null && bTime == null) {
// If neither step has a valid end time, they're both in flight.
// So compare their start times instead.
Date cTime = a.startTime;
Date dTime = b.startTime;
// Comparison based on start time is as follows:
// If a step hasn't started yet, its startTime is null.
// That time is interpreted as ZERO for comparison purposes below.
if (cTime != null && dTime == null) {
return 1; // b (dTime) hasn't started, so b is zero and a is greater
} else if (cTime == null && dTime != null) {
return -1; // a (cTime) hasn't started, so b is greater than a
} else if (cTime == null && dTime == null) {
// no start or end times on neither, return equal.
return 0;
}
// Both start times are valid, so sort based on those values
return Long.compare(cTime.getTime(), dTime.getTime());
}
return Long.compare(aTime.getTime(), bTime.getTime());
}
});
// First element will be the root cause, the remaining elements are additional errors.
for (StepStatus status : statuses) {
if (rootCauseStatus == null) {
rootCauseStatus = status;
} else {
additionalErrors.add(status);
}
}
// Now formulate an error message that contains all side-effects
if (rootCauseStatus != null) {
// Start the message with the root cause:
StringBuffer sb = new StringBuffer(
"Message: " + rootCauseStatus.message + "\n" + "Description: " + rootCauseStatus.description + "\n");
if (!additionalErrors.isEmpty()) {
sb.append("\nAdditional errors occurred during processing. Each error is listed below.\n\n");
Iterator<StepStatus> iter = additionalErrors.iterator();
while (iter.hasNext()) {
StepStatus ss = iter.next();
sb.append("Additional Message: " + ss.message + "\n" + "Description: " + ss.description + "\n");
if (iter.hasNext()) {
sb.append("\n");
}
}
}
// Assemble the resulting error service code with the compiled message.
error = ServiceError.buildServiceError(rootCauseStatus.serviceCode, sb.toString());
}
return error;
}
public Map<String, Step> getStepMap() {
return _stepMap;
}
void setStepMap(Map<String, Step> stepMap) {
this._stepMap = stepMap;
}
Map<String, Set<String>> getStepGroupMap() {
return _stepGroupMap;
}
void setStepGroupMap(Map<String, Set<String>> stepGroupMap) {
this._stepGroupMap = stepGroupMap;
}
public String getOrchControllerName() {
return _orchControllerName;
}
public String getOrchMethod() {
return _orchMethod;
}
public String getOrchTaskId() {
return _orchTaskId;
}
Map<String, StepStatus> getStepStatusMap() {
return _stepStatusMap;
}
public void setStepStatusMap(Map<String, StepStatus> stepStatusMap) {
this._stepStatusMap = stepStatusMap;
}
public Boolean getRollbackContOnError() {
return _rollbackContOnError;
}
public void setRollbackContOnError(Boolean rollbackContOnError) {
this._rollbackContOnError = rollbackContOnError;
}
public Boolean isRollbackState() {
return _rollbackState;
}
public void setRollbackState(Boolean rollbackState) {
this._rollbackState = rollbackState;
}
public URI getWorkflowURI() {
return _workflowURI;
}
public void setWorkflowURI(URI _workflowURI) {
this._workflowURI = _workflowURI;
}
public WorkflowService getService() {
return _service;
}
public void setService(WorkflowService _service) {
this._service = _service;
}
public boolean isSuspendOnError() {
return _suspendOnError;
}
public void setSuspendOnError(boolean suspendOnError) {
this._suspendOnError = suspendOnError;
}
public WorkflowState getWorkflowState() {
return _workflowState;
}
public void setWorkflowState(WorkflowState workflowState) {
this._workflowState = workflowState;
}
public Set<URI> getSuspendSteps() {
return _suspendSteps;
}
public void setSuspendSteps(Set<URI> suspendSteps) {
this._suspendSteps = suspendSteps;
}
/**
* If the step is defined in this workflow, attachs the suspendedMessage to it,
* which will be displayed when this step is suspended.
* @param stepId -- The step id of the step to be annotated
* @param suspendedMessage -- The message to be displayed if the step is suspended
*/
public void setSuspendedStepMessage(String stepId, String suspendedMessage) {
Step step = getStepMap().get(stepId);
if (step == null) {
return;
}
step.suspendedMessage = suspendedMessage;
}
public boolean isTreatSuspendRollbackAsTerminate() {
return _treatSuspendRollbackAsTerminate;
}
public void setTreatSuspendRollbackAsTerminate(boolean treatSuspendRollbackAsTerminate) {
this._treatSuspendRollbackAsTerminate = treatSuspendRollbackAsTerminate;
}
public boolean isRollingBackFromSuspend() {
return _rollingBackFromSuspend;
}
public void setRollingBackFromSuspend(boolean rollingBackFromSuspend) {
this._rollingBackFromSuspend = rollingBackFromSuspend;
}
}